diff --git a/components/MedicalReport.tsx b/components/MedicalReport.tsx index 6b60adb..f29815d 100644 --- a/components/MedicalReport.tsx +++ b/components/MedicalReport.tsx @@ -391,6 +391,74 @@ const getStyles = (isRtl: boolean) => { textAlign: textStart, width: '100%', }, + pricingSection: { + marginTop: 12, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: BRAND.slate100, + }, + pricingLabel: { + fontSize: 8.5, + fontFamily: f(true), + color: '#047857', + marginBottom: 6, + textAlign: textStart, + letterSpacing: isRtl ? 0 : 0.6, + textTransform: labelTransform, + width: '100%', + }, + priceBox: { + backgroundColor: '#ecfdf5', + borderWidth: 1, + borderColor: '#a7f3d0', + borderRadius: 10, + paddingVertical: 8, + paddingHorizontal: 12, + marginBottom: 8, + }, + priceText: { + fontSize: 14, + fontFamily: f(true), + color: '#065f46', + textAlign: textStart, + }, + altRow: { + flexDirection: isRtl ? 'row-reverse' : 'row', + alignItems: 'flex-start', + marginBottom: 6, + padding: 8, + borderRadius: 8, + borderWidth: 1, + borderColor: BRAND.slate100, + backgroundColor: BRAND.slate50, + }, + altName: { + fontSize: 10, + fontFamily: f(true), + color: BRAND.slate900, + textAlign: textStart, + }, + altDetail: { + fontSize: 8.5, + fontFamily: f(false), + color: BRAND.slate500, + textAlign: textStart, + marginTop: 2, + }, + altPrice: { + fontSize: 9, + fontFamily: f(true), + color: '#047857', + textAlign: textStart, + marginTop: 2, + }, + pricingDisclaimer: { + fontSize: 7.5, + fontFamily: f(false), + color: BRAND.slate400, + marginTop: 6, + textAlign: textStart, + }, interactionBox: { marginBottom: 12, padding: 16, @@ -447,12 +515,26 @@ const getStyles = (isRtl: boolean) => { }); }; +interface PriceEstimatePDF { + price: number; + currency: string; +} + +interface EgyptianAlternativePDF { + name: string; + manufacturer: string; + estimatedPrice: PriceEstimatePDF; + note: string; +} + interface Medication { name: string; dosage: string; usage: string; tip: string; reminders: { time: string; label: string }[]; + estimatedPrice?: PriceEstimatePDF; + egyptianAlternatives?: EgyptianAlternativePDF[]; } interface Interaction { @@ -484,6 +566,10 @@ interface MedicalReportProps { severityDisplayMedium: string; severityDisplayLow: string; summaryLabel: string; + pricingSection: string; + estimatedPriceLabel: string; + alternativesLabel: string; + pricingDisclaimer: string; }; } @@ -600,6 +686,43 @@ export const MedicalReport = ({ ) : null} + + {/* Pricing & Alternatives Section */} + {(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? ( + + {labels.pricingSection} + + {med.estimatedPrice && med.estimatedPrice.price > 0 ? ( + + + {labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price} + + + ) : null} + + {med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? ( + + {labels.alternativesLabel} + {med.egyptianAlternatives.map((alt, ai) => ( + + + {alt.name} + {alt.manufacturer} + {alt.note ? {alt.note} : null} + + + + {alt.estimatedPrice.currency} {alt.estimatedPrice.price} + + + + ))} + + ) : null} + + {labels.pricingDisclaimer} + + ) : null} ))} diff --git a/components/results/MedicationCard.tsx b/components/results/MedicationCard.tsx index 24e2d4e..b1071bc 100644 --- a/components/results/MedicationCard.tsx +++ b/components/results/MedicationCard.tsx @@ -5,6 +5,7 @@ import { AlertTriangle, Clock, Stethoscope, ExternalLink } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import { formatTime12h } from '@/lib/format-time'; import type { Medication } from '@/lib/types/prescription'; +import { MedicationPricing } from './MedicationPricing'; type MedicationCardProps = { medication: Medication; @@ -40,6 +41,13 @@ export function MedicationCard({ medication, index }: MedicationCardProps) { {medication.dosage} + + {/* Inline price badge */} + {medication.estimatedPrice && medication.estimatedPrice.price > 0 && ( + + 💊 {medication.estimatedPrice.currency} {medication.estimatedPrice.price} + + )} ) : null} + + diff --git a/components/results/MedicationPricing.tsx b/components/results/MedicationPricing.tsx new file mode 100644 index 0000000..946aa3d --- /dev/null +++ b/components/results/MedicationPricing.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { useState } from 'react'; +import { motion, AnimatePresence } from 'motion/react'; +import { Factory, ExternalLink, TrendingDown, Info, Tag, Sparkles, ChevronDown, ChevronRight } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import type { PriceEstimate, EgyptianAlternative } from '@/lib/types/prescription'; + +type MedicationPricingProps = { + estimatedPrice?: PriceEstimate; + egyptianAlternatives?: EgyptianAlternative[]; + medicationName: string; +}; + +export function MedicationPricing({ + estimatedPrice, + egyptianAlternatives, + medicationName, +}: MedicationPricingProps) { + const t = useTranslations('Insights'); + const [isExpanded, setIsExpanded] = useState(false); + + const hasPrice = estimatedPrice && estimatedPrice.price > 0; + const hasAlternatives = egyptianAlternatives && egyptianAlternatives.length > 0; + + if (!hasPrice && !hasAlternatives) return null; + + // Calculate max savings from alternatives + const maxSavings = + hasPrice && hasAlternatives + ? Math.max( + 0, + ...egyptianAlternatives.map( + (alt) => estimatedPrice.price - alt.estimatedPrice.price + ) + ) + : 0; + + return ( +
+ {/* ── Price Display ─────────────────────────────────────────── */} + {hasPrice && ( + + {/* Subtle decorative glow */} +
+ +
+
+
+ +

+ {t('estimatedPrice')} +

+
+

+ + {estimatedPrice.currency} + + {estimatedPrice.price} +

+ + {/* Savings teaser / Toggle Trigger */} + {maxSavings > 0 && ( + + )} +
+ +
+ 💊 +
+
+ + )} + + {/* ── Egyptian Alternatives ──────────────────────────────────── */} + {hasAlternatives && ( +
+ {/* Section Header with Toggle */} + + + {/* Collapsible Content */} + + {isExpanded && ( + +
+ {egyptianAlternatives.map((alt, idx) => { + const savings = + hasPrice + ? Math.round(estimatedPrice.price - alt.estimatedPrice.price) + : 0; + + return ( + + {/* Best savings highlight */} + {savings > 0 && idx === 0 && ( + + + )} + +
+ )} + + {/* ── Disclaimer ─────────────────────────────────────────────── */} +
+ +

+ {t('priceDisclaimer')} +

+
+
+ ); +} diff --git a/hooks/useScanner.tsx b/hooks/useScanner.tsx index 26d557e..3479d48 100644 --- a/hooks/useScanner.tsx +++ b/hooks/useScanner.tsx @@ -165,6 +165,10 @@ export function useScanner() { severityDisplayMedium: t('Insights.severityDisplayMedium'), severityDisplayLow: t('Insights.severityDisplayLow'), summaryLabel: t('Report.summaryLabel'), + pricingSection: t('Report.pricingSection'), + estimatedPriceLabel: t('Report.estimatedPriceLabel'), + alternativesLabel: t('Report.alternativesLabel'), + pricingDisclaimer: t('Report.pricingDisclaimer'), }; const blob = await pdf( diff --git a/lib/gemini.ts b/lib/gemini.ts index 962bce0..ca0dfe0 100644 --- a/lib/gemini.ts +++ b/lib/gemini.ts @@ -4,8 +4,8 @@ import { GoogleGenAI, Type } from "@google/genai"; const GEMINI_API_KEY = process.env.GEMINI_API_KEY; // The new @google/genai SDK initializes with an options object -const client = GEMINI_API_KEY - ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) +const client = GEMINI_API_KEY + ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) : null; const ROSHETTA_PROMPT = ` @@ -28,16 +28,35 @@ You are "Roshetta.AI" (روشتة.ذكاء), a world-class digital pharmacist po - Evaluate all medications for potential clinical interactions. - Set severity to "High", "Medium", or "Low". - Provide a clear explanation of the risk and recommended action in the requested language. -5. **Localization**: - - Respond in the language of the provided locale (Arabic or English). - - For Arabic, use a warm, reassuring, and professional Egyptian tone. +5. **Medication Pricing (Egyptian Market)**: + - Provide an **approximate price** (as of your knowledge cutoff or available data) for medications in Egypt. + - **Disclaimer**: In the root 'disclaimer' field, you MUST include a note that "Prices are estimates and may not reflect current market rates" in the appropriate language, along with the mandatory medical disclaimer. + - **Accuracy**: If real-time accuracy is required, the system should ideally call an external pricing API. If you cannot determine a price with reasonable confidence, you MUST omit the \`estimatedPrice\` field or set it to null. + - **Format**: \`estimatedPrice\` must be an object: \`{ "price": number, "currency": "EGP" }\`. + - **Example**: \`estimatedPrice: { "price": 97, "currency": "EGP" }\`. Be precise with concentrations and pack sizes. + - Always set currency to "EGP". +6. **Egyptian Alternatives (IMPORTANT — Always provide when available)**: + - For EVERY imported or branded medication, you MUST suggest up to 3 locally manufactured Egyptian generic alternatives. This is a critical feature for Egyptian patients who need affordable options. + - **Always search your knowledge** for Egyptian-made generics with the same active ingredient and strength. + - For each alternative provide: + - **name**: The exact Egyptian brand name (e.g., "Hibiotic" instead of "Augmentin", "Cetal" instead of "Panadol", "Antinal" instead of "Ercefuryl"). + - **manufacturer**: The Egyptian pharmaceutical company (e.g., "Amoun Pharmaceutical", "EIPICO", "Pharco", "EVA Pharma", "Sedico", "GlaxoSmithKline Egypt", "Medical Union Pharmaceuticals", "Memphis Pharma", "Kahira Pharma", "Delta Pharma", "Nile Pharma", "Marcyrl Pharma"). + - **estimatedPrice**: An object with the approximate \`price\` (number) and \`currency\` ("EGP") for this alternative. Omit if uncertain. + - **note**: Clearly state the active ingredient match (e.g., "نفس المادة الفعالة: أموكسيسيللين + كلافيولانيك أسيد 1 جم" or "Same active ingredient: Amoxicillin/Clavulanate 1g"). + - If the medication IS already a local Egyptian product, include one entry with the same name and note: "هذا منتج مصري محلي بالفعل" / "This is already a local Egyptian product". + - Only return an empty array if there genuinely are no Egyptian-made alternatives (very rare for common medications). + - **Prioritize the cheapest alternatives first** in the array. +7. **Localization**: + - Respond in the language of the provided locale (Arabic or English). + - For Arabic, use a warm, reassuring, and professional Egyptian tone. # Summary Field -Provide a "summary" field that briefly describes the overall purpose of the prescription (e.g., "Prescription for seasonal allergy and cough" or "مجموعة أدوية لعلاج نزلات البرد والاحتقان"). +Provide a "summary" field that briefly describes the overall purpose of the prescription. -# Disclaimer -Mandatory Arabic ending: "هذا التحليل بالذكاء الاصطناعي للمساعدة فقط. يجب التأكد من الجرعات مع الصيدلي عند شراء الدواء." -Mandatory English ending: "This AI analysis is for assistance only. Dosages must be confirmed with a pharmacist when purchasing the medication." +# Disclaimer Field +In the "disclaimer" field of the JSON: +- Mandatory Arabic: "هذا التحليل بالذكاء الاصطناعي للمساعدة فقط. يجب التأكد من الجرعات مع الصيدلي عند شراء الدواء. الأسعار استرشادية وقد لا تعكس أسعار السوق الحالية." +- Mandatory English: "This AI analysis is for assistance only. Dosages must be confirmed with a pharmacist. Prices are estimates and may not reflect current market rates." # Output Format Return a valid JSON object following the defined schema. @@ -67,6 +86,36 @@ const schema = { required: ["time", "label"], }, }, + estimatedPrice: { + type: Type.OBJECT, + properties: { + price: { type: Type.NUMBER }, + currency: { type: Type.STRING }, + }, + required: ["price", "currency"], + nullable: true, + }, + egyptianAlternatives: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + name: { type: Type.STRING }, + manufacturer: { type: Type.STRING }, + estimatedPrice: { + type: Type.OBJECT, + properties: { + price: { type: Type.NUMBER }, + currency: { type: Type.STRING }, + }, + required: ["price", "currency"], + nullable: true, + }, + note: { type: Type.STRING }, + }, + required: ["name", "manufacturer", "note"], + }, + }, }, required: ["name", "dosage", "usage", "tip", "reminders"], }, @@ -91,7 +140,10 @@ const schema = { * Analyzes an image of a prescription using Gemini AI. * This function should ONLY be called on the server. */ -export async function analyzePrescriptionImage(base64Image: string, locale: string = 'en') { +export async function analyzePrescriptionImage( + base64Image: string, + locale: string = "en", +) { if (!client) { throw new Error("Gemini API key is not configured."); } @@ -100,7 +152,7 @@ export async function analyzePrescriptionImage(base64Image: string, locale: stri // The new @google/genai SDK uses client.models.generateContent directly const response = await client.models.generateContent({ - model: "gemini-3-flash-preview", + model: "gemini-3-flash-preview", contents: [ { text: prompt }, { diff --git a/lib/types/prescription.ts b/lib/types/prescription.ts index daf2df6..05faecb 100644 --- a/lib/types/prescription.ts +++ b/lib/types/prescription.ts @@ -3,12 +3,26 @@ export interface MedicationReminder { label: string; } +export interface PriceEstimate { + price: number; + currency: string; // "EGP" +} + +export interface EgyptianAlternative { + name: string; + manufacturer: string; + estimatedPrice: PriceEstimate; + note: string; // e.g., "Same active ingredient (Amoxicillin)" +} + export interface Medication { name: string; dosage: string; usage: string; tip: string; reminders: MedicationReminder[]; + estimatedPrice?: PriceEstimate; + egyptianAlternatives?: EgyptianAlternative[]; } export interface Interaction { diff --git a/messages/ar.json b/messages/ar.json index 4d1bd5d..bff9d5a 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -64,7 +64,19 @@ "copyResults": "نسخ التحليل", "searchWeb": "بحث عن الدواء", "copied": "تم النسخ!", - "sharingText": "إليك تحليل روشتتي بواسطة Roshetta-AI:\n\n" + "sharingText": "إليك تحليل روشتتي بواسطة Roshetta-AI:\n\n", + "estimatedPrice": "السعر التقديري", + "priceRange": "{price} ج.م", + "egyptianAlternatives": "البدائل المصرية", + "alternativesHint": "بدائل محلية الصنع بنفس المادة الفعالة", + "manufacturerLabel": "إنتاج {manufacturer}", + "savingsLabel": "وفّر حتى {amount} ج.م", + "noAlternatives": "لم يتم تحديد بدائل محلية", + "priceDisclaimer": "الأسعار تقديرية بالذكاء الاصطناعي وقد تختلف بين الصيدليات", + "showPricing": "الأسعار والبدائل", + "hidePricing": "إخفاء الأسعار", + "searchAlternative": "بحث", + "localProduct": "منتج محلي" }, "Report": { "title": "تقرير Roshetta.AI", @@ -81,7 +93,11 @@ "dateLabel": "التاريخ", "timeLabel": "الوقت", "summaryLabel": "ملخص الحالة", - "footerTagline": "Roshetta.AI · تحليل بمساعدة الذكاء الاصطناعي فقط. يُرجى دائمًا التأكد مع صيدلي أو طبيب مرخّص." + "footerTagline": "Roshetta.AI · تحليل بمساعدة الذكاء الاصطناعي فقط. يُرجى دائمًا التأكد مع صيدلي أو طبيب مرخّص.", + "pricingSection": "الأسعار والبدائل المصرية", + "estimatedPriceLabel": "السعر التقديري", + "alternativesLabel": "البدائل المحلية", + "pricingDisclaimer": "الأسعار تقديرية وقد تختلف" }, "Loading": { "parsingImage": "تحسين جودة الصورة...", diff --git a/messages/en.json b/messages/en.json index 0224f9e..ae12ec4 100644 --- a/messages/en.json +++ b/messages/en.json @@ -64,7 +64,19 @@ "copyResults": "Copy Results", "searchWeb": "Search Info", "copied": "Copied to clipboard!", - "sharingText": "Check out my decoded prescription from Roshetta-AI:\n\n" + "sharingText": "Check out my decoded prescription from Roshetta-AI:\n\n", + "estimatedPrice": "Estimated Price", + "priceRange": "EGP {price}", + "egyptianAlternatives": "Egyptian Alternatives", + "alternativesHint": "Locally manufactured options with the same active ingredient", + "manufacturerLabel": "by {manufacturer}", + "savingsLabel": "Save up to EGP {amount}", + "noAlternatives": "No local alternatives identified", + "priceDisclaimer": "Prices are AI-estimated and may vary between pharmacies", + "showPricing": "Pricing & Alternatives", + "hidePricing": "Hide Pricing", + "searchAlternative": "Search", + "localProduct": "Local Product" }, "Report": { "title": "Roshetta.AI Report", @@ -81,7 +93,11 @@ "dateLabel": "Date", "timeLabel": "Time", "summaryLabel": "Medical Summary", - "footerTagline": "Roshetta.AI · AI-assisted analysis only. Always confirm with a licensed pharmacist or physician." + "footerTagline": "Roshetta.AI · AI-assisted analysis only. Always confirm with a licensed pharmacist or physician.", + "pricingSection": "Pricing & Egyptian Alternatives", + "estimatedPriceLabel": "Est. Price", + "alternativesLabel": "Local Alternatives", + "pricingDisclaimer": "Prices are AI-estimated and may vary" }, "Loading": { "parsingImage": "Optimizing image...", diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.