Deploy: 2026-05-22 00:54:36
This commit is contained in:
@@ -72,4 +72,65 @@ class ChatbotController extends BaseController
|
||||
'id' => $id
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate prompt from voice recording using Gemini audio input
|
||||
*/
|
||||
public function generatePromptFromAudio(Request $request, Response $response)
|
||||
{
|
||||
// 1. Get the uploaded audio file
|
||||
$files = $_FILES;
|
||||
if (empty($files['audio']) || $files['audio']['error'] !== UPLOAD_ERR_OK) {
|
||||
$response->status(400)->json(['status' => 'error', 'message' => 'Missing or invalid audio file upload']);
|
||||
return;
|
||||
}
|
||||
|
||||
$audioFile = $files['audio'];
|
||||
|
||||
// 2. Validate file size (max 10MB)
|
||||
if ($audioFile['size'] > 10 * 1024 * 1024) {
|
||||
$response->status(400)->json(['status' => 'error', 'message' => 'Audio file size exceeds limit (10MB)']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Read the audio content and base64 encode
|
||||
$audioContent = file_get_contents($audioFile['tmp_name']);
|
||||
if ($audioContent === false) {
|
||||
$response->status(500)->json(['status' => 'error', 'message' => 'Failed to read uploaded audio file']);
|
||||
return;
|
||||
}
|
||||
$audioBase64 = base64_encode($audioContent);
|
||||
$mimeType = $audioFile['type'] ?: 'audio/webm'; // fallback
|
||||
// Normalize mime type (e.g. "audio/webm;codecs=opus" -> "audio/webm") for Gemini API compatibility
|
||||
if (strpos($mimeType, ';') !== false) {
|
||||
$mimeType = trim(explode(';', $mimeType)[0]);
|
||||
}
|
||||
|
||||
// 4. Retrieve the Gemini API key (custom or system default)
|
||||
$rules = ChatbotRule::findAllByCompany($request->company_id);
|
||||
$apiKey = null;
|
||||
if (!empty($rules) && !empty($rules[0]['gemini_api_key'])) {
|
||||
$apiKey = $rules[0]['gemini_api_key'];
|
||||
}
|
||||
|
||||
$apiKey = $apiKey ?: getenv('GEMINI_API_KEY');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
$response->status(500)->json(['status' => 'error', 'message' => 'Gemini API Key is not configured in the system']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Call Gemini Service
|
||||
$generatedPrompt = \App\Services\GeminiService::generatePromptFromAudio($apiKey, $audioBase64, $mimeType);
|
||||
|
||||
if (empty($generatedPrompt)) {
|
||||
$response->status(500)->json(['status' => 'error', 'message' => 'Failed to generate prompt from audio. Please check Gemini logs.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'prompt' => trim($generatedPrompt)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +314,9 @@ class WhatsAppController extends BaseController
|
||||
}
|
||||
$systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.';
|
||||
|
||||
// Enforce language matching rule dynamically
|
||||
$systemPrompt .= "\n\nIMPORTANT LANGUAGE RULE: Detect the language of the incoming message. If the incoming message is in English, you MUST reply in English. If the incoming message is in Arabic, you MUST reply in Arabic. Override any default language instruction to match the user's language.";
|
||||
|
||||
$replyText = \App\Services\GeminiService::generateResponse($apiKey, $systemPrompt, $incomingText);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,4 +48,52 @@ class GeminiService
|
||||
$data = json_decode($response, true);
|
||||
return $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Gemini API with audio inline data to generate a chatbot prompt
|
||||
*/
|
||||
public static function generatePromptFromAudio(string $apiKey, string $audioBase64, string $mimeType): ?string
|
||||
{
|
||||
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=' . $apiKey;
|
||||
|
||||
$payload = json_encode([
|
||||
'contents' => [
|
||||
[
|
||||
'role' => 'user',
|
||||
'parts' => [
|
||||
[
|
||||
'inlineData' => [
|
||||
'mimeType' => $mimeType,
|
||||
'data' => $audioBase64
|
||||
]
|
||||
],
|
||||
[
|
||||
'text' => "أنت خبير محترف في هندسة التعليمات (Prompt Engineering). استمع جيداً للتسجيل الصوتي المرفق الذي يصف متجراً أو مشروعاً تجارياً ومتطلبات خدمة العملاء، واستخرج التفاصيل المهمة (اسم المتجر، الخدمات، اللهجة المطلوبة، ساعات العمل، سياسات الشحن والاستبدال، والأسئلة الشائعة). ثم قم بصياغة تعليمة نظام (System Instruction Prompt) مفصلة ومنظمة وعالية الجودة باللغة العربية لروبوت خدمة العملاء المعتمد على الذكاء الاصطناعي. يجب أن ترشد التعليمة الروبوت بكيفية التصرف والرد بنبرة مناسبة. أرجع فقط تعليمة النظام الناتجة مباشرة بدون أي نصوص تمهيدية أو تنسيقات markdown أو علامات اقتباس برمجية."
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 35); // 35 seconds timeout for audio analysis
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
error_log("[Gemini Audio API Error] HTTP " . $httpCode . " | Response: " . $response);
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
return $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,6 +618,14 @@
|
||||
}
|
||||
.font-semibold { font-weight: 600; }
|
||||
.text-muted { color: var(--text-secondary); }
|
||||
@keyframes pulse-red {
|
||||
0% { transform: scale(0.85); opacity: 0.5; }
|
||||
50% { transform: scale(1.15); opacity: 1; }
|
||||
100% { transform: scale(0.85); opacity: 0.5; }
|
||||
}
|
||||
.recording-pulse {
|
||||
animation: pulse-red 1.2s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body x-data="app()" x-init="checkAuth()">
|
||||
@@ -1005,8 +1013,41 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="chatbot-prompt-group">
|
||||
<label class="form-label" x-text="chatbotSettings.trigger_type === 'gemini_ai' ? 'System Instruction Prompt' : 'Predefined Auto-Reply Message'"></label>
|
||||
<textarea class="form-input" x-model="chatbotSettings.ai_prompt" rows="5" required :placeholder="chatbotSettings.trigger_type === 'gemini_ai' ? 'You are a helpful customer support assistant... Respond concisely and politely in Arabic.' : 'Thank you for reaching out!'" id="chatbot-prompt-input"></textarea>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<label class="form-label" x-text="chatbotSettings.trigger_type === 'gemini_ai' ? 'System Instruction Prompt' : 'Predefined Auto-Reply Message'" style="margin-bottom: 0;"></label>
|
||||
<div x-show="chatbotSettings.trigger_type === 'gemini_ai'" style="display: flex; gap: 0.35rem; flex-wrap: wrap; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('nabeh')">قالب تطبيق نبيه (سوري)</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('store')">قالب متجر إلكتروني</button>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto;" @click="loadPromptTemplate('general')">قالب عام (إنجليزي)</button>
|
||||
|
||||
<!-- Voice Recording Button -->
|
||||
<button type="button" class="btn" :class="isRecording ? 'btn-danger' : 'btn-secondary'" style="font-size: 0.75rem; padding: 0.25rem 0.5rem; width: auto; display: flex; align-items: center; gap: 0.25rem;" @click="toggleVoiceRecording()">
|
||||
<svg x-show="!isRecording" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
||||
<span x-show="isRecording" class="recording-pulse" style="display: inline-block; width: 8px; height: 8px; background-color: white; border-radius: 50%;"></span>
|
||||
<span x-text="isRecording ? 'إيقاف التسجيل (' + formatRecordTime(recordingTime) + ')' : '🎤 تسجيل الإرشادات'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="form-input" x-model="chatbotSettings.ai_prompt" rows="7" required :placeholder="chatbotSettings.trigger_type === 'gemini_ai' ? 'أدخل تعليمات الذكاء الاصطناعي هنا...' : 'شكراً لتواصلك معنا!'" id="chatbot-prompt-input" :disabled="generatingFromVoice"></textarea>
|
||||
|
||||
<!-- Voice Processing Loading Status -->
|
||||
<div x-show="generatingFromVoice" style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--primary-accent);" dir="rtl">
|
||||
<span class="spinner" style="border-top-color: var(--primary-accent); display: inline-block;"></span>
|
||||
<span>جاري صياغة التوجيهات من صوتك باستخدام الذكاء الاصطناعي... يرجى الانتظار</span>
|
||||
</div>
|
||||
|
||||
<!-- Hints & Guidelines for Merchant -->
|
||||
<div x-show="chatbotSettings.trigger_type === 'gemini_ai'" style="margin-top: 0.75rem; padding: 0.75rem; background: rgba(59, 130, 246, 0.05); border: 1px dashed rgba(59, 130, 246, 0.3); border-radius: 8px; font-size: 0.8rem; color: var(--text-secondary);" dir="rtl">
|
||||
<strong style="color: var(--text-primary); display: block; margin-bottom: 0.35rem; display: flex; align-items: center; gap: 0.35rem;">
|
||||
💡 نصائح وتوجيهات لكتابة تعليمات ممتازة:
|
||||
</strong>
|
||||
<ul style="list-style-type: disc; margin-right: 1.25rem; padding-left: 0; line-height: 1.5; margin-bottom: 0;">
|
||||
<li><strong>الهوية والاسم:</strong> حدد اسم الروبوت بوضوح (مثال: "أنا سارة من فريق تطبيق نبيه").</li>
|
||||
<li><strong>اللهجة والأسلوب:</strong> اطلب من الذكاء الاصطناعي الرد بلهجة معينة (مثال: اللهجة السورية أو الفصحى المبسطة).</li>
|
||||
<li><strong>البيانات الأساسية:</strong> اكتب ساعات عمل المتجر، طرق الدفع والتوصيل، وسياسة الاستبدال لكي يجيب الروبوت بدقة.</li>
|
||||
<li><strong>التعليمات اللغوية:</strong> قمنا بتضمين ميزة مطابقة اللغة تلقائياً (الرد بالإنجليزية على الرسائل الإنجليزية، وبالعربية على العربية).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" :disabled="actionLoading" id="chatbot-save-btn">
|
||||
@@ -1209,6 +1250,14 @@
|
||||
|
||||
groups: [],
|
||||
|
||||
// Voice Recording States
|
||||
isRecording: false,
|
||||
recordingTime: 0,
|
||||
recordingIntervalId: null,
|
||||
mediaRecorder: null,
|
||||
audioChunks: [],
|
||||
generatingFromVoice: false,
|
||||
|
||||
chatbotSettings: {
|
||||
is_active: '0',
|
||||
trigger_type: 'keyword',
|
||||
@@ -1570,6 +1619,37 @@
|
||||
}
|
||||
},
|
||||
|
||||
loadPromptTemplate(type) {
|
||||
if (type === 'nabeh') {
|
||||
this.chatbotSettings.ai_prompt = `أنتِ "سارة"، موظفة خدمة العملاء الافتراضية الذكية والودودة لتطبيق "نبيه" (Nabeh).
|
||||
مهمتكِ هي مساعدة المستخدمين والإجابة على استفساراتهم بلطف وأدب بالمسائل التقنية والتجارية المتعلقة بالتطبيق.
|
||||
|
||||
اتبعي القواعد التالية بدقة عند الرد:
|
||||
1. في أول رسالة تواصل مع العميل، عرّفي عن نفسكِ دائماً بالقول: "معك سارة من فريق تطبيق نبيه، كيف بقدر أساعدك اليوم؟" (باللهجة السورية الودية).
|
||||
2. تحدثي دائماً باللهجة السورية اللطيفة والترحيبية والودية عند التحدث باللغة العربية.
|
||||
3. إذا سأل العميل أو تحدث باللغة الإنجليزية، فتحدثي معه باللغة الإنجليزية بشكل احترافي وودي وحافظي على نفس الهوية والمساعدة.
|
||||
4. كوني مختصرة ومباشرة في إجاباتكِ وتجنبي الإطالة غير الضرورية.
|
||||
5. ساعدي المستخدمين في فهم ميزات تطبيق "نبيه" لإدارة وتسهيل حملات واتساب والردود الذكية.`;
|
||||
} else if (type === 'store') {
|
||||
this.chatbotSettings.ai_prompt = `أنت موظف خدمة عملاء ذكي ومرحب لمتجرنا الإلكتروني.
|
||||
مهمتك هي مساعدة العملاء والإجابة على استفساراتهم حول المنتجات، الشحن، والطلبات بلطف وأدب.
|
||||
|
||||
اتبع القواعد التالية عند الرد:
|
||||
1. رحب بالعميل دائماً بشكل ودّي وسريع في بداية المحادثة.
|
||||
2. أجب عن الأسئلة بدقة واختصار.
|
||||
3. إذا سأل العميل عن حالة طلب، اطلب منه تزويدك برقم الطلب للتحقق منه.
|
||||
4. حافظ على نبرة إيجابية ومحترفة.`;
|
||||
} else if (type === 'general') {
|
||||
this.chatbotSettings.ai_prompt = `You are a professional and helpful customer support assistant.
|
||||
Your goal is to assist users with their general inquiries, troubleshoot issues, and provide helpful guidance.
|
||||
|
||||
Guidelines:
|
||||
1. Be polite, clear, and professional.
|
||||
2. Provide concise and accurate information.
|
||||
3. If you do not know the answer, politely ask the user to wait while you check with the team.`;
|
||||
}
|
||||
},
|
||||
|
||||
async saveChatbotSettings() {
|
||||
this.actionLoading = true;
|
||||
try {
|
||||
@@ -1603,6 +1683,98 @@
|
||||
}
|
||||
},
|
||||
|
||||
formatRecordTime(seconds) {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
||||
},
|
||||
|
||||
async toggleVoiceRecording() {
|
||||
if (this.isRecording) {
|
||||
await this.stopVoiceRecording();
|
||||
} else {
|
||||
await this.startVoiceRecording();
|
||||
}
|
||||
},
|
||||
|
||||
async startVoiceRecording() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
this.audioChunks = [];
|
||||
this.mediaRecorder = new MediaRecorder(stream);
|
||||
|
||||
this.mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data && event.data.size > 0) {
|
||||
this.audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaRecorder.onstop = async () => {
|
||||
const mimeType = this.mediaRecorder.mimeType || 'audio/webm';
|
||||
const audioBlob = new Blob(this.audioChunks, { type: mimeType });
|
||||
// Stop all audio tracks to release microphone
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
await this.sendAudioToBackend(audioBlob);
|
||||
};
|
||||
|
||||
this.mediaRecorder.start();
|
||||
this.isRecording = true;
|
||||
this.recordingTime = 0;
|
||||
|
||||
this.recordingIntervalId = setInterval(() => {
|
||||
this.recordingTime++;
|
||||
if (this.recordingTime >= 180) { // 3 minutes max limit
|
||||
this.stopVoiceRecording();
|
||||
}
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
console.error('Error starting audio recording:', err);
|
||||
alert('تعذر الوصول إلى الميكروفون. يرجى التحقق من صلاحيات المتصفح.');
|
||||
}
|
||||
},
|
||||
|
||||
async stopVoiceRecording() {
|
||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
||||
this.mediaRecorder.stop();
|
||||
}
|
||||
if (this.recordingIntervalId) {
|
||||
clearInterval(this.recordingIntervalId);
|
||||
this.recordingIntervalId = null;
|
||||
}
|
||||
this.isRecording = false;
|
||||
},
|
||||
|
||||
async sendAudioToBackend(audioBlob) {
|
||||
this.generatingFromVoice = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'voice_instruction.webm');
|
||||
|
||||
const response = await fetch('/api/chatbot/generate-prompt-from-audio', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
this.chatbotSettings.ai_prompt = data.prompt;
|
||||
// Automatically save the generated prompt in database
|
||||
await this.saveChatbotSettings();
|
||||
alert('تم توليد التوجيهات وحفظها بنجاح!');
|
||||
} else {
|
||||
alert(data.message || 'فشلت عملية صياغة التوجيهات من الصوت.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error sending audio to backend:', err);
|
||||
alert('حدث خطأ أثناء التواصل مع السيرفر لصياغة التوجيهات.');
|
||||
} finally {
|
||||
this.generatingFromVoice = false;
|
||||
}
|
||||
},
|
||||
|
||||
openAddContactModal() {
|
||||
this.contactName = '';
|
||||
this.contactPhone = '';
|
||||
|
||||
@@ -66,6 +66,7 @@ $router->post('/api/campaigns', [\App\Controllers\CampaignController::class,
|
||||
|
||||
$router->get('/api/chatbot/rules', [\App\Controllers\ChatbotController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/chatbot/rules',[\App\Controllers\ChatbotController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
$router->post('/api/chatbot/generate-prompt-from-audio', [\App\Controllers\ChatbotController::class, 'generatePromptFromAudio'], [\App\Middlewares\AuthMiddleware::class]);
|
||||
|
||||
// 4. Dispatch the request
|
||||
$router->dispatch($request, $response);
|
||||
|
||||
Reference in New Issue
Block a user