Update: 2026-08-08 17:35:50

This commit is contained in:
Hamza-Ayed
2026-08-08 17:35:50 +03:00
parent 8c9f729ecd
commit e63f990aea
18 changed files with 261 additions and 75 deletions
+40 -1
View File
@@ -371,9 +371,48 @@ class SiroGeminiService {
return null;
}
$data = json_decode($response, true);
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
return trim(preg_replace('/```html|```/', '', $text));
}
/**
* Answers a driver support inquiry acting as Siro's smart assistant.
*/
public function answerSupportInquiry(
string $question,
array $driverContext,
string $model = 'gemini-flash-lite-latest'
): ?string {
if (!$this->apiKey) return null;
$contextJson = json_encode($driverContext, JSON_UNESCAPED_UNICODE);
$prompt = "
أنت مساعد الدعم الفني الذكي لتطبيق 'سيرو' (Siro) لنقل الركاب.
وظيفتك مساعدة السائقين (الكباتن) والإجابة على استفساراتهم باحترافية، ودقة، وبلهجة ودية ومهذبة جداً.
بيانات السائق الحالية (سياق مهم للإجابة على سؤاله):
$contextJson
سؤال السائق:
\"$question\"
المطلوب منك:
1. الإجابة مباشرة على سؤال السائق بناءً على السياق المعطى إذا كان يخص رحلاته أو رصيده المذكور في السياق.
2. إذا سأل عن إجراءات عامة، أجب كخبير في سيرو.
3. اجعل الإجابة ودية وواضحة جداً وقصيرة.
4. الرد يكون باللغة العربية حصراً.
5. لا تذكر تفاصيل تقنية (مثل json أو database)، بل تحدث كإنسان يقدم الدعم.
";
$prompt .= "\n\nالرجاء إرجاع الإجابة بصيغة JSON فقط كالتالي:\n{\n \"reply\": \"نص الإجابة هنا\"\n}";
$response = $this->callGemini($prompt, $model);
if ($response && isset($response['reply'])) {
return $response['reply'];
}
return null;
}
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
require_once __DIR__ . '/../../connect.php';
require_once __DIR__ . '/../../core/Services/SiroGeminiService.php';
require_once __DIR__ . '/../../encrypt_decrypt.php';
// Variables from connect.php (JWT token)
global $user_id, $role, $con;
if (!$user_id) {
jsonError("Unauthorized");
exit;
}
$driverID = $user_id;
$helpQuestion = filterRequest("helpQuestion");
if (empty($helpQuestion)) {
jsonError("Missing parameters");
exit;
}
try {
global $encryptionHelper;
// 1. Fetch driver info (Decrypting names and phone)
$stmt = $con->prepare("SELECT `first_name`, `last_name`, `phone` FROM `driver` WHERE `id` = :id");
$stmt->bindParam(':id', $driverID);
$stmt->execute();
$driverRow = $stmt->fetch(PDO::FETCH_ASSOC);
$driverInfo = [];
if ($driverRow) {
$driverInfo = [
'first_name' => $driverRow['first_name'] ? $encryptionHelper->decryptData($driverRow['first_name']) : '',
'last_name' => $driverRow['last_name'] ? $encryptionHelper->decryptData($driverRow['last_name']) : '',
'phone' => $driverRow['phone'] ? $encryptionHelper->decryptData($driverRow['phone']) : '',
'user_type' => $role
];
}
// 2. Fetch wallet balance using S2S
$walletServer = getenv('PAYMENT_SERVER_URL') ?: 'https://wallet.siromove.com';
$walletUrl = "$walletServer/v2/main/ride/driverWallet/get_s2s_wallet_dashboard.php";
$ch = curl_init($walletUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(["driverID" => $driverID]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY')
]
]);
$s2sRes = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$totalWallet = 0.0;
if ($httpCode === 200 && $s2sRes) {
$resDecoded = json_decode($s2sRes, true);
if ($resDecoded && isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
$totalWallet = (float)($resDecoded['message']['totalWallet'] ?? 0.0);
}
}
$walletInfo = ['balance' => $totalWallet];
// 3. Fetch recent trips (Correct table is `ride` and correct columns)
$recentTrips = [];
try {
$stmt3 = $con->prepare("SELECT `id`, `date`, `time`, `status`, `price`, `paymentMethod` FROM `ride` WHERE `driver_id` = :id ORDER BY `id` DESC LIMIT 3");
$stmt3->bindParam(':id', $driverID);
$stmt3->execute();
$recentTrips = $stmt3->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $e) {
// Ignored
}
$contextData = [
'driver_info' => $driverInfo,
'wallet_balance' => $walletInfo,
'recent_trips' => $recentTrips,
];
// 4. Call Gemini
$geminiService = new SiroGeminiService();
$aiReply = $geminiService->answerSupportInquiry($helpQuestion, $contextData);
if (!$aiReply) {
$aiReply = "مرحباً كابتن، استلمنا استفسارك ولكن المساعد الذكي غير متاح حالياً. سيقوم فريق الدعم بمراجعة رسالتك قريباً.";
}
// 5. Insert into helpCenter
$sql = "INSERT INTO `helpCenter` (`driverID`, `helpQuestion`, `replay`) VALUES (:driverID, :helpQuestion, :replay)";
$stmt4 = $con->prepare($sql);
$stmt4->bindParam(':driverID', $driverID);
$stmt4->bindParam(':helpQuestion', $helpQuestion);
$stmt4->bindParam(':replay', $aiReply);
$stmt4->execute();
if ($stmt4->rowCount() > 0) {
jsonSuccess(null, "Help question saved and answered");
} else {
jsonError("Failed to save help question");
}
} catch (Exception $e) {
jsonError("An error occurred while processing your request.");
}
?>
+1 -1
View File
@@ -469,7 +469,7 @@ class AppLink {
static String get updateTips => "$ride/tips/update.php";
//-----------------Help Center------------------
static String get addhelpCenter => "$ride/helpCenter/add.php";
static String get addhelpCenter => "$ride/helpCenter/ask_gemini.php";
static String get gethelpCenter => "$ride/helpCenter/get.php";
static String get getByIdhelpCenter => "$ride/helpCenter/getById.php";
static String get updatehelpCenter => "$ride/helpCenter/update.php";
@@ -5,11 +5,8 @@ import 'package:get/get.dart';
import '../../../../constant/box_name.dart';
import '../../../../constant/links.dart';
import '../../../../constant/style.dart';
import '../../../../main.dart';
import '../../../../views/widgets/elevated_btn.dart';
import '../../../functions/crud.dart';
import '../../../functions/encrypt_decrypt.dart';
class HelpController extends GetxController {
bool isLoading = false;
@@ -20,6 +17,9 @@ class HelpController extends GetxController {
String status = '';
String qustion = '';
late int indexQuestion = 0;
Map<int, String> aiReplies = {};
getIndex(int i, String qustion1) async {
indexQuestion = i;
qustion = qustion1;
@@ -38,13 +38,15 @@ class HelpController extends GetxController {
update();
if (d['status'].toString() == 'success') {
getHelpQuestion();
// Get.snackbar('Feedback data saved successfully'.tr, '',
// backgroundColor: AppColor.greenColor,
// snackPosition: SnackPosition.BOTTOM);
helpQuestionController.clear();
}
}
void getHelpQuestion() async {
getHelpQuestionFuture();
}
Future<void> getHelpQuestionFuture() async {
isLoading = true;
update();
var res = await CRUD().get(link: AppLink.gethelpCenter, payload: {
@@ -52,27 +54,9 @@ class HelpController extends GetxController {
});
if (res == "failure") {
isLoading = false;
helpQuestionDate = {'message': []};
update();
Get.defaultDialog(
title: 'There is no help Question here'.tr,
titleStyle: AppStyle.title,
middleText: '',
confirm: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
MyElevatedButton(
title: 'Add Question'.tr,
onPressed: () {
Get.back();
}),
MyElevatedButton(
title: 'Back'.tr,
onPressed: () {
Get.back();
Get.back();
}),
],
));
return;
}
helpQuestionDate = jsonDecode(res);
isLoading = false;
@@ -89,14 +73,20 @@ class HelpController extends GetxController {
status = 'not yet';
isLoading = false;
update();
} else {
status = 'done';
helpQuestionRepleyDate = jsonDecode(res);
isLoading = false;
update();
}
helpQuestionRepleyDate = jsonDecode(res);
isLoading = false;
update();
}
@override
void onInit() {
final storedReplies = box.read<Map<String, dynamic>>('ai_replies') ?? {};
aiReplies = storedReplies
.map((key, value) => MapEntry(int.parse(key), value.toString()));
getHelpQuestion();
super.onInit();
}
@@ -2717,4 +2717,52 @@ final Map<String, String> ar_jo = {
"My Earnings": "أرباحي",
"Upcoming Scheduled Rides": "الرحلات المجدولة القادمة",
"Retry": "إعادة المحاولة",
// Insurance
"Cancelling stops future premiums. Outstanding dues remain payable.": "يؤدي الإلغاء إلى إيقاف الأقساط المستقبلية. تبقى المستحقات السابقة واجبة الدفع.",
"Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.": "لم يتم إطلاق خطط التأمين في منطقتك بعد. بمجرد توفر خطة الشريك، ستظهر هنا تلقائياً.",
"No insurance plans available yet": "لا توجد خطط تأمين متاحة بعد",
"Outstanding": "المستحق",
"Settled": "المدفوع",
"Cancel policy": "إلغاء الوثيقة",
"Subscribe": "اشتراك",
"Not eligible yet": "غير مؤهل بعد",
"Premium ledger": "سجل الأقساط",
"Insurance": "التأمين",
"Premium": "القسط",
"Started on": "بدأ في",
"Policy number": "رقم الوثيقة",
"rides": "رحلة",
"Refresh": "تحديث",
// Scheduled rides
"Could not load scheduled rides": "تعذر تحميل الرحلات المجدولة",
"Please check your connection and try again.": "يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
"You have no upcoming scheduled rides": "ليس لديك أي رحلات مجدولة قادمة",
"Starting Point": "نقطة الانطلاق",
"Destination": "الوجهة",
"Currency": "دينار أردني",
// Invites
"Invite": "دعوة",
"Drivers": "سائقون",
"Passengers": "ركاب",
"Your Permanent Referral Code": "رمز الدعوة الدائم الخاص بك",
"Share via WhatsApp Groups": "شارك عبر مجموعات الواتساب",
"Enter Inviter's Code": "أدخل رمز الداعي",
"If someone referred you, enter their code below to link accounts and receive rewards.": "إذا قام شخص ما بدعوتك، أدخل رمزه أدناه لربط الحسابات والحصول على المكافآت.",
"Enter Code (e.g. AB1234)": "أدخل الرمز (مثال: AB1234)",
"Link": "ربط",
"Please enter a referral code": "يرجى إدخال رمز الدعوة",
"Enter phone": "أدخل رقم الهاتف",
"Send Invite": "إرسال دعوة",
"Show Invitations": "عرض الدعوات",
"Invitations Sent": "الدعوات المرسلة",
"No invitation found yet!": "لم يتم العثور على أي دعوات بعد!",
"Trip": "رحلة",
"Choose from contact": "اختر من جهات الاتصال",
"Invite another driver and both get a gift after he completes 100 trips!": "ادعُ سائقًا آخر واحصل كليكما على هدية بعد إكماله 100 رحلة!",
"Share this code with passengers and earn rewards when they use it!": "شارك هذا الرمز مع الركاب واكسب مكافآت عند استخدامه!",
// Help Center
"No questions asked yet.": "لم يتم طرح أي أسئلة بعد.",
};
@@ -0,0 +1 @@
// This file is no longer used. Gemini is now handled in the PHP backend.
@@ -7,6 +7,8 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:siro_driver/controller/auth/captin/history_captain.dart';
import 'package:siro_driver/controller/functions/launch.dart';
import '../../../widgets/ui/siro_ui.dart';
class HistoryDetailsPage extends StatefulWidget {
const HistoryDetailsPage({super.key});
@@ -32,13 +34,8 @@ class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
title: Text('Trip Details'.tr),
backgroundColor: Colors.white,
elevation: 1,
),
return SiroPage(
title: 'Trip Details'.tr,
body: GetBuilder<HistoryCaptainController>(
builder: (controller) {
if (controller.isloading) {
@@ -256,12 +253,9 @@ class _DetailCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.05),
margin: const EdgeInsets.only(bottom: 16.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: SiroCard(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -440,7 +440,7 @@ class UserHeader extends StatelessWidget {
),
otherAccountsPictures: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
@@ -88,6 +88,13 @@ class HelpDetailsReplayPage extends StatelessWidget {
replayData == null ||
EncryptionHelper.instance.decryptData(replayData['replay']).toString() == 'not yet';
if (isNoReply && helpController.aiReplies.containsKey(helpController.indexQuestion)) {
return Text(
helpController.aiReplies[helpController.indexQuestion]!,
style: Theme.of(context).textTheme.bodyLarge,
);
}
return Text(
isNoReply
? 'No Response yet.'.tr
@@ -28,6 +28,7 @@ class DriverInsurancePage extends StatelessWidget {
onRefresh: c.refreshAll,
color: SiroUi.interactive(context),
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: SiroUi.pageInsets,
children:
c.hasPolicy ? _policyView(context, c) : _plansView(context, c),
@@ -157,8 +158,6 @@ class DriverInsurancePage extends StatelessWidget {
message:
'Insurance plans have not been launched in your area yet. Once a partner plan is available it will appear here automatically.'
.tr,
actionLabel: 'Refresh'.tr,
onAction: c.isLoading.value ? null : () => c.refreshAll(),
),
];
}
@@ -39,6 +39,7 @@ class DriverScheduledRidesPage extends StatelessWidget {
onRefresh: controller.fetch,
color: SiroUi.interactive(context),
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: SiroUi.pageInsets,
itemCount: controller.bookings.length,
itemBuilder: (context, index) {
@@ -8,6 +8,7 @@ import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
import '../../widgets/ui/siro_ui.dart';
// --- ملاحظات هامة ---
// 1. تأكد من إضافة الرابط الجديد إلى ملف AppLink الخاص بك:
@@ -162,8 +163,8 @@ class _EcashDriverPaymentScreenState extends State<EcashDriverPaymentScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Complete Payment'.tr)),
return SiroPage(
title: 'Complete Payment'.tr,
body: WebViewWidget(controller: _controller),
);
}
@@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../widgets/ui/siro_ui.dart';
class PaymentScreenCliq extends StatefulWidget {
final double amount;
@@ -89,9 +90,8 @@ class _PaymentScreenCliqState extends State<PaymentScreenCliq> with SingleTicker
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(title: const Text("Cliq Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black),
return SiroPage(
title: "Cliq Payment",
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
@@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../widgets/ui/siro_ui.dart';
class PaymentScreenMtn extends StatefulWidget {
final double amount;
@@ -89,9 +90,8 @@ class _PaymentScreenMtnState extends State<PaymentScreenMtn> with SingleTickerPr
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(title: const Text("MTN Payment"), centerTitle: true, backgroundColor: Colors.white, foregroundColor: Colors.black),
return SiroPage(
title: "MTN Payment",
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20.0),
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import '../../../constant/finance_design_system.dart';
import '../../../controller/home/captin/behavior_controller.dart';
import '../../widgets/ui/siro_ui.dart';
class BehaviorPage extends StatelessWidget {
const BehaviorPage({super.key});
@@ -12,23 +13,11 @@ class BehaviorPage extends StatelessWidget {
final controller = Get.put(DriverBehaviorController());
controller.fetchDriverBehavior();
return Scaffold(
backgroundColor: FinanceDesignSystem.backgroundColor,
appBar: AppBar(
title: Text('Driver Behavior'.tr,
style: TextStyle(
fontWeight: FontWeight.bold,
color: FinanceDesignSystem.primaryDark)),
centerTitle: true,
backgroundColor: FinanceDesignSystem.cardColor,
elevation: 0,
iconTheme: IconThemeData(color: FinanceDesignSystem.primaryDark),
),
return SiroPage(
title: 'Driver Behavior'.tr,
body: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(
color: FinanceDesignSystem.accentBlue));
return const SiroLoading();
}
double score = controller.overallScore.value;
@@ -31,8 +31,8 @@ class EarningsPage extends StatelessWidget {
Widget build(BuildContext context) {
// شريط العنوان يأتي من الثيم — كان يبني ألوانه يدوياً في كل صفحة،
// فيختلف ارتفاعه ووزن خطه من شاشة لأخرى.
return Scaffold(
appBar: AppBar(title: Text('My Earnings'.tr)),
return SiroPage(
title: 'My Earnings'.tr,
body: GetBuilder<EarningsController>(
builder: (ctl) {
if (ctl.isLoading && ctl.summary == null) {
@@ -18,9 +18,9 @@ class StatSummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
// ‏رأسي 12 لا 16: البطاقة داخل شبكة بارتفاع ثابت، و16 كانت تتجاوزه
// ‏بنحو 10px مع خط الرقم مقاس 22.
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
// ‏رأسي 8 لا 12: البطاقة داخل شبكة بارتفاع ثابت، و12 كانت تتجاوزه
// ‏قليلاً مع خط الرقم مقاس 22.
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: FinanceDesignSystem.cardColor,
borderRadius: BorderRadius.circular(FinanceDesignSystem.cardRadius),
+8
View File
@@ -1109,6 +1109,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.0"
google_generative_ai:
dependency: "direct main"
description:
name: google_generative_ai
sha256: "71f613d0247968992ad87a0eb21650a566869757442ba55a31a81be6746e0d1f"
url: "https://pub.dev"
source: hosted
version: "0.4.7"
graphs:
dependency: transitive
description: