diff --git a/resources/js/pages/apps/chat.vue b/resources/js/pages/apps/chat.vue
index 0429acd..9147408 100644
--- a/resources/js/pages/apps/chat.vue
+++ b/resources/js/pages/apps/chat.vue
@@ -103,6 +103,285 @@ const undoTimer = ref(null);
const undoProgress = ref(100);
const undoInterval = ref(null);
+const formatRecordingTime = (seconds) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+};
+
+
+// Voice Recording
+const isRecording = ref(false);
+const mediaRecorder = ref(null);
+const audioChunks = ref([]);
+const recordingStartTime = ref(null);
+const recordingDuration = ref(0);
+const recordingInterval = ref(null);
+const voiceMessage = ref(null);
+const audioStream = ref(null);
+const audioContext = ref(null);
+const analyser = ref(null);
+const dataArray = ref(null);
+const animationFrame = ref(null);
+const waveformData = ref(new Array(50).fill(0));
+const waveformCanvas = ref(null);
+const canvasCtx = ref(null);
+
+
+const startRecording = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ audioStream.value = stream;
+ mediaRecorder.value = new MediaRecorder(stream);
+ audioChunks.value = [];
+
+ audioContext.value = new (window.AudioContext || window.webkitAudioContext)();
+ analyser.value = audioContext.value.createAnalyser();
+ analyser.value.smoothingTimeConstant = 0.9;
+ const source = audioContext.value.createMediaStreamSource(stream);
+
+ analyser.value.fftSize = 2048;
+ analyser.value.smoothingTimeConstant = 0.7;
+ source.connect(analyser.value);
+
+ const bufferLength = analyser.value.fftSize;
+ dataArray.value = new Uint8Array(bufferLength);
+
+ mediaRecorder.value.ondataavailable = (event) => {
+ audioChunks.value.push(event.data);
+ };
+
+ mediaRecorder.value.onstop = () => {
+ const audioBlob = new Blob(audioChunks.value, { type: "audio/wav" });
+ voiceMessage.value = audioBlob;
+ stopAudioAnalysis();
+ };
+
+ mediaRecorder.value.start();
+ isRecording.value = true;
+ recordingStartTime.value = Date.now();
+
+ recordingInterval.value = setInterval(() => {
+ recordingDuration.value = Math.floor((Date.now() - recordingStartTime.value) / 1000);
+ }, 1000);
+
+ nextTick(() => {
+ if (waveformCanvas.value) {
+ canvasCtx.value = waveformCanvas.value.getContext("2d");
+ drawWaveform();
+ }
+ });
+ } catch (error) {
+ console.error("Error accessing microphone:", error);
+ }
+};
+
+
+const drawWaveform = () => {
+ if (!isRecording.value || !analyser.value || !canvasCtx.value) return;
+
+ analyser.value.getByteTimeDomainData(dataArray.value);
+
+ const canvas = waveformCanvas.value;
+ const ctx = canvasCtx.value;
+ const width = canvas.width;
+ const height = canvas.height;
+
+ ctx.clearRect(0, 0, width, height);
+
+ ctx.lineWidth = 2;
+ ctx.strokeStyle = `rgb(${getComputedStyle(document.documentElement)
+ .getPropertyValue(`--v-theme-${activeModelColor.value}`)
+ .trim()})`;
+
+ ctx.beginPath();
+
+ const sliceWidth = (width * 1.0) / dataArray.value.length;
+ let x = 0;
+
+ for (let i = 0; i < dataArray.value.length; i++) {
+ const v = dataArray.value[i] / 128.0;
+ const y = (v * height) / 2;
+
+ if (i === 0) {
+ ctx.moveTo(x, y);
+ } else {
+ ctx.lineTo(x, y);
+ }
+
+ x += sliceWidth;
+ }
+
+ ctx.lineTo(width, height / 2);
+ ctx.stroke();
+
+ animationFrame.value = requestAnimationFrame(drawWaveform);
+};
+
+
+const animateWaveform = () => {
+ if (!isRecording.value || !analyser.value) return;
+
+ analyser.value.getByteFrequencyData(dataArray.value);
+
+ const chunkSize = Math.floor(dataArray.value.length / 50);
+
+ for (let i = 0; i < 50; i++) {
+ let sum = 0;
+ const start = i * chunkSize;
+ const end = Math.min(start + chunkSize, dataArray.value.length);
+
+ for (let j = start; j < end; j++) {
+ sum += dataArray.value[j];
+ }
+
+ const average = sum / (end - start);
+ let normalizedValue = (average / 255) * 100;
+
+ waveformData.value[i] = waveformData.value[i] * 0.7 + normalizedValue * 0.3;
+ }
+
+ updateWaveformBars();
+ animationFrame.value = requestAnimationFrame(() => animateWaveform());
+};
+
+const updateWaveformBars = () => {
+ const bars = document.querySelectorAll('.waveform-bar');
+ bars.forEach((bar, index) => {
+ if (index < waveformData.value.length) {
+ const height = Math.max(6, Math.min(20, 6 + (waveformData.value[index] * 0.14)));
+ bar.style.height = `${height}px`;
+ }
+ });
+};
+
+
+const stopRecording = () => {
+ if (mediaRecorder.value && mediaRecorder.value.state !== 'inactive') {
+ mediaRecorder.value.stop();
+ isRecording.value = false;
+
+ if (recordingInterval.value) {
+ clearInterval(recordingInterval.value);
+ recordingInterval.value = null;
+ }
+ }
+};
+
+const cancelRecording = () => {
+ if (mediaRecorder.value && mediaRecorder.value.state !== 'inactive') {
+ mediaRecorder.value.onstop = null;
+ mediaRecorder.value.stop();
+ }
+
+ stopAudioAnalysis();
+
+ if (audioStream.value) {
+ audioStream.value.getTracks().forEach(track => track.stop());
+ audioStream.value = null;
+ }
+
+ if (recordingInterval.value) {
+ clearInterval(recordingInterval.value);
+ recordingInterval.value = null;
+ }
+
+ mediaRecorder.value = null;
+ isRecording.value = false;
+ voiceMessage.value = null;
+ recordingDuration.value = 0;
+ recordingStartTime.value = null;
+ waveformData.value = new Array(50).fill(0);
+ audioChunks.value = [];
+};
+const stopAudioAnalysis = () => {
+ if (animationFrame.value) {
+ cancelAnimationFrame(animationFrame.value);
+ animationFrame.value = null;
+ }
+ if (audioContext.value && audioContext.value.state !== 'closed') {
+ audioContext.value.close();
+ audioContext.value = null;
+ }
+ if (audioStream.value) {
+ audioStream.value.getTracks().forEach(track => track.stop());
+ audioStream.value = null;
+ }
+};
+
+const sendVoiceMessage = async () => {
+ if (!voiceMessage.value) return;
+
+ isQuickRepliesVisible.value = false;
+
+ isRequestCancelled.value = false;
+ if (isInputCentered.value) isInputCentered.value = false;
+
+ messageQueue.value.push({
+ text: "Voice message sent",
+ sender: "user",
+ isVoiceMessage: true,
+ voiceBlob: voiceMessage.value,
+ duration: recordingDuration.value
+ });
+
+ const voiceBlob = voiceMessage.value;
+ const duration = recordingDuration.value;
+ voiceMessage.value = null;
+ recordingDuration.value = 0;
+
+ await nextTick();
+ scrollToBottom();
+
+ if (!isProcessingQueue.value && messageQueue.value.length > 0) {
+ processMessageQueue();
+ }
+
+ isBotTyping.value = true;
+ abortController.value = new AbortController();
+
+ try {
+ const formData = new FormData();
+ formData.append('voice', voiceBlob, 'voice_message.wav');
+
+ const response = await fetch("/dev/voice", {
+ method: "POST",
+ body: formData,
+ signal: abortController.value.signal,
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ const responseText = result.response || result.message || result;
+ const cleanText = cleanHtmlResponse(responseText);
+ messageQueue.value.push({ text: cleanText, sender: "bot" });
+ } else {
+ messageQueue.value.push({
+ text: "خطا در پردازش پیام صوتی. لطفاً دوباره امتحان کنید.",
+ sender: "bot",
+ });
+ }
+ } catch (error) {
+ console.error("Voice API Error:", error);
+ if (error.name !== "AbortError") {
+ messageQueue.value.push({
+ text: "خطا در ارسال پیام صوتی. لطفاً اتصال را چک کنید.",
+ sender: "bot",
+ });
+ }
+ } finally {
+ isBotTyping.value = false;
+ }
+
+ await nextTick();
+ scrollToBottom();
+};
+
+const isVoiceRecordingAvailable = computed(() => {
+ return !isInputDisabled.value && !isConfirmationActive.value && !activeForm.value && !activeMultiForm.value;
+});
+
+
// Suggestion database
const suggestionDatabase = {
پروژه: ["New Project ایجاد کنم", "پروژه وب سایت", "پروژه اپلیکیشن موبایل", "پروژه سیستم مدیریت", "پروژه فروشگاه آنلاین", "پروژه های انجام شده", "پروژه در حال اجرا"],
@@ -254,6 +533,11 @@ const findNextValidStep = (form, currentStepIndex, formData) => {
return null;
};
+const rtlRegex = /[\u0600-\u06FF\u0750-\u077F]/;
+function getDir(str = "") {
+ return rtlRegex.test(str) ? "rtl" : "ltr";
+}
+
const getSuggestions = (input) => {
if (!input || input.length < 2) return [];
@@ -473,6 +757,11 @@ const startMultiForm = (formId) => {
const sendMessage = async () => {
if (isConfirmationActive.value) return;
+ if (voiceMessage.value) {
+ await sendVoiceMessage();
+ return;
+ }
+
const userMsg = msg.value.trim();
if (!userMsg && selectedAttachments.value.length === 0) return;
@@ -485,7 +774,10 @@ const sendMessage = async () => {
if (userMsg === "/cancel") {
activeForm.value = null;
activeMultiForm.value = null;
- messageQueue.value.push({ text: "The form has been cancelled", sender: "bot" });
+ messageQueue.value.push({
+ text: "The form has been cancelled",
+ sender: "bot",
+ });
msg.value = "";
return;
}
@@ -493,16 +785,27 @@ const sendMessage = async () => {
isRequestCancelled.value = false;
if (isInputCentered.value) isInputCentered.value = false;
- // Handle file uploads
if (selectedAttachments.value.length > 0) {
- const fileNames = selectedAttachments.value.map(f => f.name).join(", ");
+ const fileNames = selectedAttachments.value.map((f) => f.name).join(", ");
messageQueue.value.push({
text: userMsg || `Files sent: ${fileNames}`,
sender: "user",
isAttachment: true,
- files: [...selectedAttachments.value]
+ files: [...selectedAttachments.value],
});
+
selectedAttachments.value = [];
+ msg.value = "";
+
+ messageQueue.value.push({
+ text: "فایلها با موفقیت دریافت شد ✅",
+ sender: "bot",
+ isSystemMessage: true
+ });
+
+ await nextTick();
+ scrollToBottom();
+ return;
} else {
messageQueue.value.push({ text: userMsg, sender: "user" });
}
@@ -515,7 +818,6 @@ const sendMessage = async () => {
processMessageQueue();
}
- // Handle different message types
if (activeMultiForm.value) {
const formResponse = handleMultiFormResponse(userMsg);
if (formResponse.completed) {
@@ -526,35 +828,42 @@ const sendMessage = async () => {
messageQueue.value.push({
sender: "bot",
multiForm: formResponse.nextStep,
- type: "multi-form"
+ type: "multi-form",
});
}
+ return;
} else if (activeForm.value) {
const formResponse = handleFormResponse(userMsg);
messageQueue.value.push({ text: formResponse, sender: "bot" });
activeForm.value = null;
+ return;
} else {
const botReply = getBotReply(userMsg);
if (botReply && typeof botReply === "object") {
if (botReply.type === "multi-form") {
isBotTyping.value = true;
- await new Promise(r => setTimeout(r, 500));
- activeMultiForm.value = botReply.formId;
- currentFormStep.value = 0;
- multiFormData.value = {};
- const form = forms[botReply.formId];
- messageQueue.value.push({
- sender: "bot",
- multiForm: {
- ...form.steps[0],
- stepNumber: 1,
- totalSteps: form.steps.length,
- formTitle: form.title
- },
- type: "multi-form"
- });
- isBotTyping.value = false;
+ try {
+ await new Promise((r) => setTimeout(r, 500));
+ activeMultiForm.value = botReply.formId;
+ currentFormStep.value = 0;
+ multiFormData.value = {};
+ const form = forms[botReply.formId];
+ messageQueue.value.push({
+ sender: "bot",
+ multiForm: {
+ ...form.steps[0],
+ stepNumber: 1,
+ totalSteps: form.steps.length,
+ formTitle: form.title,
+ },
+ type: "multi-form",
+ });
+ } catch (error) {
+ console.error("Multi-form setup error:", error);
+ } finally {
+ isBotTyping.value = false;
+ }
} else if (botReply.type === "form") {
activeForm.value = botReply.formId;
const form = forms[botReply.formId];
@@ -565,43 +874,75 @@ const sendMessage = async () => {
id: form.id,
title: form.title,
question: form.question,
- options: form.options
+ options: form.options,
},
- type: "form"
+ type: "form",
});
}
return;
} else if (typeof botReply === "string") {
messageQueue.value.push({ text: botReply, sender: "bot" });
+ return;
} else {
- // API call
isBotTyping.value = true;
abortController.value = new AbortController();
+ const SHOW_GENERIC_ERROR_IN_CHAT = true;
+
try {
- const response = await fetch("/CallChatService.php", {
+ console.log("Sending message to server:", userMsg);
+
+ const response = await fetch("/dev/chatbot", {
method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: new URLSearchParams({ message: userMsg }),
- signal: abortController.value.signal
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({ message: userMsg }),
+ signal: abortController.value.signal,
});
+ console.log("Response status:", response.status);
+ console.log("Response ok:", response.ok);
+
if (response.ok) {
- const resultText = await response.text();
- const cleanText = cleanHtmlResponse(resultText);
+ const result = await response.json();
+ const responseText = result.response || result.message || result;
+ const cleanText = cleanHtmlResponse(responseText);
messageQueue.value.push({ text: cleanText, sender: "bot" });
} else {
- messageQueue.value.push({ text: "Error receiving response from server", sender: "bot" });
+ let serverText = "";
+ try {
+ serverText = await response.text();
+ } catch (e) { }
+ console.error(`Server error (${response.status})`, serverText);
+ if (SHOW_GENERIC_ERROR_IN_CHAT) {
+ messageQueue.value.push({
+ text: "چتبات با مشکل روبهرو شد. لطفاً دوباره امتحان کن.",
+ sender: "bot",
+ });
+ }
+ return;
}
- } catch {
- if (!isRequestCancelled.value) {
- messageQueue.value.push({ text: "Could not connect to the server", sender: "bot" });
+ } catch (error) {
+ console.error("Chatbot API Error:", error);
+ if (error.name === "AbortError") {
+ console.log("Request was aborted");
+ return;
}
+ if (SHOW_GENERIC_ERROR_IN_CHAT) {
+ messageQueue.value.push({
+ text: "چتبات با مشکل روبهرو شد. لطفاً اتصال را چک کن و دوباره تلاش کن.",
+ sender: "bot",
+ });
+ }
+ return;
+ } finally {
+ isBotTyping.value = false;
}
}
}
- isBotTyping.value = false;
await nextTick();
scrollToBottom();
};
@@ -1134,6 +1475,9 @@ watch([activeForm, activeMultiForm, isConfirmationActive], ([form, multiForm, co
if (form !== null || multiForm !== null || confirmation) {
console.log('Quick replies hidden due to active form');
}
+ if (voiceMessage.value) {
+ voiceMessage.value = null;
+ }
});
// Lifecycle
@@ -1147,6 +1491,7 @@ const models = MODELS;
const commands = COMMANDS;
+