Feature: medication pricing#1
Conversation
- Update pricing instructions to request "approximate prices" instead of real-time market rates. - Add mandatory user-facing disclaimers in the prompt stating that prices are estimates. - Align prompt examples and descriptions for `estimatedPrice` with the actual JSON schema (object format). - Refine uncertainty handling to allow omitting pricing data or setting it to null when confidence is low. - Fix TypeScript syntax error in `lib/gemini.ts` by escaping nested backticks in the prompt template string. - Consolidate medical and pricing disclaimers into the root `disclaimer` output field.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces a medication pricing and Egyptian local alternatives feature throughout the application. It extends data models with pricing types, updates the Gemini API prompt to request structured pricing and alternative medication data, and adds UI components to display pricing badges, collapsible cards, and local alternative suggestions with savings calculations. Changes
Sequence DiagramsequenceDiagram
actor User
participant MedicationCard as MedicationCard<br/>(Client)
participant MedicationPricing as MedicationPricing<br/>(Display)
participant Gemini as Gemini API<br/>(Data Source)
User->>Gemini: Upload prescription image
Gemini->>Gemini: Extract & structure pricing<br/>+ alternatives
Gemini-->>MedicationCard: Return Medication[]<br/>with pricing data
MedicationCard->>MedicationCard: Render price badge<br/>(if price > 0)
MedicationCard->>MedicationPricing: Pass estimatedPrice<br/>+ egyptianAlternatives
MedicationPricing->>MedicationPricing: Render price card<br/>with toggle
MedicationPricing->>MedicationPricing: Calculate maxSavings<br/>from alternatives
MedicationPricing->>MedicationPricing: Render collapsible<br/>alternatives list
User->>MedicationPricing: Click toggles
MedicationPricing->>MedicationPricing: Animate expand/collapse
MedicationPricing-->>User: Display alternatives<br/>+ savings badges
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
lib/gemini.ts (1)
89-118: Schema/type alignment for alternatives.A few schema details to confirm against the intended product behavior (root-cause already captured against
lib/types/prescription.ts):
- The alternative's
estimatedPriceisnullable: trueand not inrequired, but the prompt at line 44 instructs "Omit if uncertain". Combined with the current TS type marking it required, downstream renderers will crash on omitted prices. Either tighten the schema to require it or relax the type.required: ["name", "manufacturer", "note"]makesnotemandatory, yetMedicationPricing.tsxandMedicalReport.tsxboth render it viaif (alt.note)/ternary, suggesting the UI expects it to be optional. Decide which side is canonical.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/gemini.ts` around lines 89 - 118, The schema for egyptianAlternatives is inconsistent with the prompt and UI: either make alternative.estimatedPrice required in the JSON schema (remove nullable and add "estimatedPrice" to required) to match a required TS type, or (preferred) relax types by keeping estimatedPrice nullable/omitted in the schema and update the TS type in lib/types/prescription.ts to make estimatedPrice optional; likewise remove "note" from egyptianAlternatives.required so note is optional (or alternatively update MedicationPricing.tsx and MedicalReport.tsx to treat note as required); pick the side of least UI disruption—make estimatedPrice and note optional in the TS type and schema to match the "Omit if uncertain" prompt and the components that check alt.note.components/results/MedicationPricing.tsx (2)
12-18:medicationNameprop is unused.It's declared in
MedicationPricingPropsand destructured but never referenced in the body. Either drop it or wire it up to something useful — e.g., as anaria-labelfor the section, or to enrich the pharmacy search URL on line 187 (currently it searches only the alternative's name; including the original drug context could help users compare against the prescribed brand):Suggested usage example
- <a - href={`https://www.google.com/search?q=${encodeURIComponent(alt.name + ' pharmacy Egypt price')}`} + <a + href={`https://www.google.com/search?q=${encodeURIComponent(`${alt.name} (${medicationName} alternative) pharmacy Egypt price`)}`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/results/MedicationPricing.tsx` around lines 12 - 18, The medicationName prop is declared and destructured in the MedicationPricing component but never used; either remove it from MedicationPricingProps/destructuring or wire it into the UI: add medicationName as an aria-label on the component's root container (e.g., "<section aria-label={medicationName}>") and/or include it when building the pharmacy search URL used in the pharmacy link logic (concatenate or include as a query param alongside the alternative's name so the search query contains both the original prescribed drug and the alternative). Update references in MedicationPricing (props and JSX) accordingly and remove the prop from the type if you choose to drop it.
67-79: Two unrelated buttons drive the sameisExpandedstate — confusing UX.The savings-chip button on the price card (line 69) and the alternatives section header (line 93) both call
setIsExpanded(!isExpanded). Side effects:
- Clicking the savings chip on the price card silently toggles the alternatives panel below, even though the chip's text ("Save up to EGP …") doesn't visually communicate "open list".
- Conversely, the chevron on the savings chip (line 76) rotates whenever the alternatives panel is opened from the header — but the chip is in a different visual region, so the rotation looks like a stray animation.
- If a future change shows the price card without alternatives (or vice versa), one of the buttons becomes a dead control still mutating shared state.
Consider either consolidating to a single primary toggle (the section header), or giving the savings chip a clearer call-to-action label (e.g., "View alternatives") so the relationship is explicit. At minimum, give the toggle button an
aria-expandedandaria-controlslinking it to the collapsible region for screen-reader users.Also applies to: 93-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/results/MedicationPricing.tsx` around lines 67 - 79, Two unrelated controls are toggling the same state (isExpanded via setIsExpanded) causing confusing UX and stray animations; update the component so only one primary toggle controls the alternatives panel (preferably the alternatives section header's toggle) and make the savings chip a non-toggle affordance or an explicit "View alternatives" action that activates the header toggle; specifically, remove or change the onClick on the savings-chip button (the element containing TrendingDown, t('savingsLabel', ...), and ChevronDown) so it no longer directly calls setIsExpanded(!isExpanded), or wire it to call a single handler (e.g., toggleAlternatives) that delegates to the header's toggle logic; additionally add aria-expanded and aria-controls attributes to the real collapsible trigger and ensure the chip's button (if still clickable) also uses aria-expanded and aria-controls pointing to the collapsible region so screen readers understand the relationship.components/results/MedicationCard.tsx (1)
44-50: Price is rendered twice per medication card.When
medication.estimatedPrice.price > 0, this inline badge displays the price (e.g. "💊 EGP 97") andMedicationPricing(line 117) renders a large prominent price panel showing the same value. That's redundant on a single card and adds visual noise for what is effectively one data point.Consider one of:
- Keep only the large panel inside
MedicationPricing(drop the inline badge), or- Keep the inline badge as the primary price surface and remove the price block from
MedicationPricing(let it focus on alternatives only).Also, the 💊 glyph here is a decorative inline emoji with no accessible name. The equivalent emoji inside
MedicationPricingis wrapped withrole="img"+aria-label. Either addaria-hidden="true"here or wrap consistently:Proposed a11y fix
- <span className="inline-flex items-center gap-1 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200/60 dark:border-emerald-700/30 px-2 py-0.5 text-[11px] font-black tabular-nums text-emerald-700 dark:text-emerald-300"> - 💊 {medication.estimatedPrice.currency} {medication.estimatedPrice.price} - </span> + <span className="inline-flex items-center gap-1 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200/60 dark:border-emerald-700/30 px-2 py-0.5 text-[11px] font-black tabular-nums text-emerald-700 dark:text-emerald-300"> + <span aria-hidden="true">💊</span> {medication.estimatedPrice.currency} {medication.estimatedPrice.price} + </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/results/MedicationCard.tsx` around lines 44 - 50, The inline price badge in MedicationCard (the span rendering medication.estimatedPrice) duplicates the price shown by MedicationPricing; remove the inline badge so the large price panel from MedicationPricing is the single price surface, and keep MedicationPricing responsible for displaying medication.estimatedPrice.price and currency. If you prefer the badge as primary instead, instead remove the price display logic inside MedicationPricing and leave it to show alternatives only. Also fix the emoji accessibility: if you keep any inline decorative emoji (💊) in MedicationCard, add aria-hidden="true" to that emoji or otherwise wrap it with role="img" and an aria-label to match MedicationPricing’s approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/MedicalReport.tsx`:
- Around line 689-725: The pricing section currently checks (med.estimatedPrice
|| (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) which
still renders when estimatedPrice.price === 0 and alternatives are empty; change
the outer guard to require the same detailed conditions used inside: only render
if (med.estimatedPrice && med.estimatedPrice.price > 0) ||
(med.egyptianAlternatives && med.egyptianAlternatives.length > 0), so the inner
blocks (the estimated price block that checks med.estimatedPrice.price > 0 and
the egyptianAlternatives.map block) are guaranteed to produce content before
showing the pricingSection, keeping labels.pricingSection and pricingDisclaimer
from rendering alone.
- Around line 518-538: Replace the locally redeclared types PriceEstimatePDF,
EgyptianAlternativePDF and Medication with the canonical exports from
lib/types/prescription.ts (import PriceEstimate, EgyptianAlternative and
Medication and use them instead of the local declarations) to avoid type drift;
update the interface references in this file to use those imported symbols,
remove the duplicate declarations, and add a null-safety guard around accesses
like alt.estimatedPrice.currency / alt.estimatedPrice.price (or narrow via
optional chaining) since estimatedPrice may be optional in the shared type.
In `@lib/gemini.ts`:
- Around line 31-37: The pricing-estimate sentence is duplicated because it's
present in the root "disclaimer" and also rendered per-medication via
labels.pricingDisclaimer and t('priceDisclaimer') in MedicalReport.tsx and
results/MedicationPricing.tsx; remove the sentence "Prices are estimates and may
not reflect current market rates" from the root `disclaimer` value (and
corresponding root-language strings) so the per-section
`labels.pricingDisclaimer` / t('priceDisclaimer') remain the single source of
truth, and update any translation entries for the root disclaimer to omit that
sentence.
In `@lib/types/prescription.ts`:
- Around line 11-16: The TypeScript interface EgyptianAlternative is too strict:
change estimatedPrice: PriceEstimate to estimatedPrice?: PriceEstimate and note:
string to note?: string in lib/types/prescription.ts to match the Gemini schema
which allows nullable/absent prices; then update callers (e.g.,
components/results/MedicationPricing.tsx and components/MedicalReport.tsx) to
either filter egyptianAlternatives for alt.estimatedPrice before computing
maxSavings/savings or add guards where used (e.g., check alt.estimatedPrice &&
alt.estimatedPrice.price and only render price/currency when present) so no code
assumes estimatedPrice is always defined.
In `@messages/en.json`:
- Around line 67-79: The mentioned translation keys (priceRange, noAlternatives,
showPricing, hidePricing, localProduct) are defined but unused; either wire them
into the UI or remove them. Fix by: (1) replace the inline price rendering with
the i18n template key priceRange (pass a single formatted price string or
numeric price as the {price} param where the current estimatedPrice rendering
occurs), (2) add an empty-state render that uses noAlternatives when the
alternatives list is empty inside the alternatives component, (3) make the
pricing toggle button text come from showPricing and hidePricing based on the
toggle state instead of hardcoded text, and (4) use localProduct as the
label/aria-label when rendering each locally manufactured alternative in the
alternatives list; if you prefer not to surface these strings, remove them from
messages/en.json and messages/ar.json to avoid translation drift.
In `@next-env.d.ts`:
- Line 3: The committed auto-generated Next.js type file next-env.d.ts
(containing the line `import "./.next/types/routes.d.ts";`) should be removed
from version control and ignored: add next-env.d.ts to .gitignore and run git rm
--cached next-env.d.ts to stop tracking it, then amend the commit so this change
is not part of the medication-pricing feature PR; ensure no code edits are made
to next-env.d.ts itself since it is auto-generated.
---
Nitpick comments:
In `@components/results/MedicationCard.tsx`:
- Around line 44-50: The inline price badge in MedicationCard (the span
rendering medication.estimatedPrice) duplicates the price shown by
MedicationPricing; remove the inline badge so the large price panel from
MedicationPricing is the single price surface, and keep MedicationPricing
responsible for displaying medication.estimatedPrice.price and currency. If you
prefer the badge as primary instead, instead remove the price display logic
inside MedicationPricing and leave it to show alternatives only. Also fix the
emoji accessibility: if you keep any inline decorative emoji (💊) in
MedicationCard, add aria-hidden="true" to that emoji or otherwise wrap it with
role="img" and an aria-label to match MedicationPricing’s approach.
In `@components/results/MedicationPricing.tsx`:
- Around line 12-18: The medicationName prop is declared and destructured in the
MedicationPricing component but never used; either remove it from
MedicationPricingProps/destructuring or wire it into the UI: add medicationName
as an aria-label on the component's root container (e.g., "<section
aria-label={medicationName}>") and/or include it when building the pharmacy
search URL used in the pharmacy link logic (concatenate or include as a query
param alongside the alternative's name so the search query contains both the
original prescribed drug and the alternative). Update references in
MedicationPricing (props and JSX) accordingly and remove the prop from the type
if you choose to drop it.
- Around line 67-79: Two unrelated controls are toggling the same state
(isExpanded via setIsExpanded) causing confusing UX and stray animations; update
the component so only one primary toggle controls the alternatives panel
(preferably the alternatives section header's toggle) and make the savings chip
a non-toggle affordance or an explicit "View alternatives" action that activates
the header toggle; specifically, remove or change the onClick on the
savings-chip button (the element containing TrendingDown, t('savingsLabel',
...), and ChevronDown) so it no longer directly calls
setIsExpanded(!isExpanded), or wire it to call a single handler (e.g.,
toggleAlternatives) that delegates to the header's toggle logic; additionally
add aria-expanded and aria-controls attributes to the real collapsible trigger
and ensure the chip's button (if still clickable) also uses aria-expanded and
aria-controls pointing to the collapsible region so screen readers understand
the relationship.
In `@lib/gemini.ts`:
- Around line 89-118: The schema for egyptianAlternatives is inconsistent with
the prompt and UI: either make alternative.estimatedPrice required in the JSON
schema (remove nullable and add "estimatedPrice" to required) to match a
required TS type, or (preferred) relax types by keeping estimatedPrice
nullable/omitted in the schema and update the TS type in
lib/types/prescription.ts to make estimatedPrice optional; likewise remove
"note" from egyptianAlternatives.required so note is optional (or alternatively
update MedicationPricing.tsx and MedicalReport.tsx to treat note as required);
pick the side of least UI disruption—make estimatedPrice and note optional in
the TS type and schema to match the "Omit if uncertain" prompt and the
components that check alt.note.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cf1b6ae6-e647-4445-8cf7-96711f30cd14
📒 Files selected for processing (9)
components/MedicalReport.tsxcomponents/results/MedicationCard.tsxcomponents/results/MedicationPricing.tsxhooks/useScanner.tsxlib/gemini.tslib/types/prescription.tsmessages/ar.jsonmessages/en.jsonnext-env.d.ts
| 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[]; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid redeclaring pricing types — import the canonical ones.
PriceEstimatePDF and EgyptianAlternativePDF are structurally identical to PriceEstimate and EgyptianAlternative in lib/types/prescription.ts, and the local Medication interface here is a structural subset of the exported Medication. Redeclaring them invites drift (e.g., if note becomes optional in the canonical type, the PDF copy will still mark it required). Reuse the shared types so changes there propagate automatically.
Proposed fix
-import { normalizeInteractionSeverity, type InteractionSeverity } from '@/lib/types/prescription';
+import {
+ normalizeInteractionSeverity,
+ type InteractionSeverity,
+ type Medication,
+ type Interaction,
+ type PriceEstimate,
+ type EgyptianAlternative,
+} from '@/lib/types/prescription';
@@
-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 {
- severity: string;
- description: string;
-}Note: the unsafe access on line 715 (alt.estimatedPrice.currency} {alt.estimatedPrice.price) is part of the broader null-safety issue raised on lib/types/prescription.ts and will need a guard once the canonical type marks estimatedPrice optional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/MedicalReport.tsx` around lines 518 - 538, Replace the locally
redeclared types PriceEstimatePDF, EgyptianAlternativePDF and Medication with
the canonical exports from lib/types/prescription.ts (import PriceEstimate,
EgyptianAlternative and Medication and use them instead of the local
declarations) to avoid type drift; update the interface references in this file
to use those imported symbols, remove the duplicate declarations, and add a
null-safety guard around accesses like alt.estimatedPrice.currency /
alt.estimatedPrice.price (or narrow via optional chaining) since estimatedPrice
may be optional in the shared type.
|
|
||
| {/* Pricing & Alternatives Section */} | ||
| {(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? ( | ||
| <View style={styles.pricingSection}> | ||
| <Text style={styles.pricingLabel}>{labels.pricingSection}</Text> | ||
|
|
||
| {med.estimatedPrice && med.estimatedPrice.price > 0 ? ( | ||
| <View style={styles.priceBox}> | ||
| <Text style={styles.priceText}> | ||
| {labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price} | ||
| </Text> | ||
| </View> | ||
| ) : null} | ||
|
|
||
| {med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? ( | ||
| <View style={{ marginTop: 4 }}> | ||
| <Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text> | ||
| {med.egyptianAlternatives.map((alt, ai) => ( | ||
| <View key={ai} style={styles.altRow}> | ||
| <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}> | ||
| <Text style={styles.altName}>{alt.name}</Text> | ||
| <Text style={styles.altDetail}>{alt.manufacturer}</Text> | ||
| {alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null} | ||
| </View> | ||
| <View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}> | ||
| <Text style={styles.altPrice}> | ||
| {alt.estimatedPrice.currency} {alt.estimatedPrice.price} | ||
| </Text> | ||
| </View> | ||
| </View> | ||
| ))} | ||
| </View> | ||
| ) : null} | ||
|
|
||
| <Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text> | ||
| </View> | ||
| ) : null} |
There was a problem hiding this comment.
Pricing section can render with no content.
The outer guard med.estimatedPrice || (med.egyptianAlternatives?.length > 0) admits the case where med.estimatedPrice is present with price === 0 and egyptianAlternatives is empty. In that scenario both inner blocks short-circuit (line 695 requires price > 0; line 703 requires non-empty alternatives) and the section ends up rendering only the section title and disclaimer. Tighten the outer guard to match:
Proposed fix
- {(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (
+ {((med.estimatedPrice && med.estimatedPrice.price > 0) || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {/* Pricing & Alternatives Section */} | |
| {(med.estimatedPrice || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? ( | |
| <View style={styles.pricingSection}> | |
| <Text style={styles.pricingLabel}>{labels.pricingSection}</Text> | |
| {med.estimatedPrice && med.estimatedPrice.price > 0 ? ( | |
| <View style={styles.priceBox}> | |
| <Text style={styles.priceText}> | |
| {labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price} | |
| </Text> | |
| </View> | |
| ) : null} | |
| {med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? ( | |
| <View style={{ marginTop: 4 }}> | |
| <Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text> | |
| {med.egyptianAlternatives.map((alt, ai) => ( | |
| <View key={ai} style={styles.altRow}> | |
| <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}> | |
| <Text style={styles.altName}>{alt.name}</Text> | |
| <Text style={styles.altDetail}>{alt.manufacturer}</Text> | |
| {alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null} | |
| </View> | |
| <View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}> | |
| <Text style={styles.altPrice}> | |
| {alt.estimatedPrice.currency} {alt.estimatedPrice.price} | |
| </Text> | |
| </View> | |
| </View> | |
| ))} | |
| </View> | |
| ) : null} | |
| <Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text> | |
| </View> | |
| ) : null} | |
| {/* Pricing & Alternatives Section */} | |
| {((med.estimatedPrice && med.estimatedPrice.price > 0) || (med.egyptianAlternatives && med.egyptianAlternatives.length > 0)) ? ( | |
| <View style={styles.pricingSection}> | |
| <Text style={styles.pricingLabel}>{labels.pricingSection}</Text> | |
| {med.estimatedPrice && med.estimatedPrice.price > 0 ? ( | |
| <View style={styles.priceBox}> | |
| <Text style={styles.priceText}> | |
| {labels.estimatedPriceLabel}: {med.estimatedPrice.currency} {med.estimatedPrice.price} | |
| </Text> | |
| </View> | |
| ) : null} | |
| {med.egyptianAlternatives && med.egyptianAlternatives.length > 0 ? ( | |
| <View style={{ marginTop: 4 }}> | |
| <Text style={[styles.pricingLabel, { marginTop: 4, marginBottom: 8 }]}>{labels.alternativesLabel}</Text> | |
| {med.egyptianAlternatives.map((alt, ai) => ( | |
| <View key={ai} style={styles.altRow}> | |
| <View style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }}> | |
| <Text style={styles.altName}>{alt.name}</Text> | |
| <Text style={styles.altDetail}>{alt.manufacturer}</Text> | |
| {alt.note ? <Text style={styles.altDetail}>{alt.note}</Text> : null} | |
| </View> | |
| <View style={{ flexShrink: 0, alignItems: isRtl ? 'flex-start' : 'flex-end' }}> | |
| <Text style={styles.altPrice}> | |
| {alt.estimatedPrice.currency} {alt.estimatedPrice.price} | |
| </Text> | |
| </View> | |
| </View> | |
| ))} | |
| </View> | |
| ) : null} | |
| <Text style={styles.pricingDisclaimer}>{labels.pricingDisclaimer}</Text> | |
| </View> | |
| ) : null} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/MedicalReport.tsx` around lines 689 - 725, The pricing section
currently checks (med.estimatedPrice || (med.egyptianAlternatives &&
med.egyptianAlternatives.length > 0)) which still renders when
estimatedPrice.price === 0 and alternatives are empty; change the outer guard to
require the same detailed conditions used inside: only render if
(med.estimatedPrice && med.estimatedPrice.price > 0) ||
(med.egyptianAlternatives && med.egyptianAlternatives.length > 0), so the inner
blocks (the estimated price block that checks med.estimatedPrice.price > 0 and
the egyptianAlternatives.map block) are guaranteed to produce content before
showing the pricingSection, keeping labels.pricingSection and pricingDisclaimer
from rendering alone.
| 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". |
There was a problem hiding this comment.
Pricing disclaimer is doubled in the rendered output.
The prompt mandates that the model append "Prices are estimates and may not reflect current market rates" (line 33) into the root disclaimer (and the mandatory English/Arabic disclaimer texts on lines 58–59 already contain this sentence). At the same time:
components/MedicalReport.tsxline 723 renders the standalonelabels.pricingDisclaimer.components/results/MedicationPricing.tsxline 210 renderst('priceDisclaimer').
Net effect: in the PDF the user sees the pricing-estimate caveat both inside the medical disclaimer block at the bottom and inside every medication's pricing section; in the app UI it appears in every medication card too. Pick one source of truth — either drop the pricing-estimate sentence from the root disclaimer text and keep the per-section pricingDisclaimer, or remove the per-section disclaimer and rely on the root one.
Also applies to: 56-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/gemini.ts` around lines 31 - 37, The pricing-estimate sentence is
duplicated because it's present in the root "disclaimer" and also rendered
per-medication via labels.pricingDisclaimer and t('priceDisclaimer') in
MedicalReport.tsx and results/MedicationPricing.tsx; remove the sentence "Prices
are estimates and may not reflect current market rates" from the root
`disclaimer` value (and corresponding root-language strings) so the per-section
`labels.pricingDisclaimer` / t('priceDisclaimer') remain the single source of
truth, and update any translation entries for the root disclaimer to omit that
sentence.
| export interface EgyptianAlternative { | ||
| name: string; | ||
| manufacturer: string; | ||
| estimatedPrice: PriceEstimate; | ||
| note: string; // e.g., "Same active ingredient (Amoxicillin)" | ||
| } |
There was a problem hiding this comment.
Type contract is stricter than the runtime data — make estimatedPrice (and note) optional.
The Gemini schema in lib/gemini.ts declares the alternative's estimatedPrice with nullable: true and lists only ["name", "manufacturer", "note"] in required. So at runtime an EgyptianAlternative can legitimately arrive without a price, but TypeScript currently believes estimatedPrice: PriceEstimate is always present.
This silently masks unsafe accesses downstream, all of which will throw a TypeError: Cannot read properties of undefined (reading 'price' / 'currency') if Gemini omits the field:
components/results/MedicationPricing.tsxline 34 (alt.estimatedPrice.priceinsidemaxSavingsreducer), line 132 (Math.round(estimatedPrice.price - alt.estimatedPrice.price)), and line 173 (alt.estimatedPrice.currency } { alt.estimatedPrice.price).components/MedicalReport.tsxline 715 (alt.estimatedPrice.currency } { alt.estimatedPrice.price) — also rendered into the PDF.
Similarly, MedicationPricing.tsx line 178 already does if (alt.note), suggesting note is treated as optional in practice; aligning the type avoids the false sense of safety.
Proposed type fix (then add the missing guards at the call sites)
export interface EgyptianAlternative {
name: string;
manufacturer: string;
- estimatedPrice: PriceEstimate;
- note: string; // e.g., "Same active ingredient (Amoxicillin)"
+ estimatedPrice?: PriceEstimate;
+ note?: string; // e.g., "Same active ingredient (Amoxicillin)"
}You'll also want to either:
- Filter
egyptianAlternativesforalt.estimatedPricebefore computingmaxSavings/savings, or - Guard each render site (e.g., only show the price badge in
MedicationPricing.tsxandMedicalReport.tsxwhenalt.estimatedPrice && alt.estimatedPrice.price > 0).
Please confirm whether the Gemini schema's nullable: true for the alternative's estimatedPrice is intentional — if alternatives are always expected to carry a price, the schema (not the type) should be tightened by adding estimatedPrice to required and removing nullable: true.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/types/prescription.ts` around lines 11 - 16, The TypeScript interface
EgyptianAlternative is too strict: change estimatedPrice: PriceEstimate to
estimatedPrice?: PriceEstimate and note: string to note?: string in
lib/types/prescription.ts to match the Gemini schema which allows
nullable/absent prices; then update callers (e.g.,
components/results/MedicationPricing.tsx and components/MedicalReport.tsx) to
either filter egyptianAlternatives for alt.estimatedPrice before computing
maxSavings/savings or add guards where used (e.g., check alt.estimatedPrice &&
alt.estimatedPrice.price and only render price/currency when present) so no code
assumes estimatedPrice is always defined.
| "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" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find any references to the suspected-unused keys across the codebase.
for key in priceRange noAlternatives showPricing hidePricing localProduct; do
echo "=== $key ==="
rg -nP --type=ts --type=tsx --type=js --type=jsx "['\"]${key}['\"]|\.${key}\b|t\(\s*['\"]\w+\.${key}['\"]\s*\)|t\(\s*['\"]${key}['\"]\s*\)" || true
doneRepository: KMS74/roshetta-ai
Length of output: 322
🏁 Script executed:
#!/bin/bash
# Search all files for references to the suspected-unused keys (no file type filter)
for key in priceRange noAlternatives showPricing hidePricing localProduct; do
echo "=== Searching for: $key ==="
rg -n "\"${key}\"|'${key}'" --max-count=5 || echo "No matches found"
doneRepository: KMS74/roshetta-ai
Length of output: 836
🏁 Script executed:
# Also find and inspect the MedicationPricing.tsx file
fd -name "MedicationPricing.tsx" -x cat -n {}Repository: KMS74/roshetta-ai
Length of output: 292
🏁 Script executed:
# Check for any translation/i18n usage patterns with these keys
rg -n "showPricing|hidePricing|noAlternatives|priceRange|localProduct" --context 3Repository: KMS74/roshetta-ai
Length of output: 2209
🏁 Script executed:
# Search for these keys being used in component files (tsx/ts)
rg -n "priceRange|noAlternatives|showPricing|hidePricing|localProduct" --type-list | head -20Repository: KMS74/roshetta-ai
Length of output: 451
🏁 Script executed:
# List all tsx/ts files to understand file types
fd -e tsx -e ts --max-depth 5 | head -20Repository: KMS74/roshetta-ai
Length of output: 597
🏁 Script executed:
# Search in all files for these keys in component/import context
rg "showPricing|hidePricing|priceRange|noAlternatives|localProduct" -t ts -t js 2>/dev/null || rg "showPricing|hidePricing|priceRange|noAlternatives|localProduct"Repository: KMS74/roshetta-ai
Length of output: 628
🏁 Script executed:
# Find and read MedicationPricing component
fd "MedicationPricing" -x head -100 {}Repository: KMS74/roshetta-ai
Length of output: 4815
🏁 Script executed:
fd "MedicationPricing" -x wc -l {}Repository: KMS74/roshetta-ai
Length of output: 108
🏁 Script executed:
fd "MedicationPricing" -x cat {}Repository: KMS74/roshetta-ai
Length of output: 11876
🏁 Script executed:
# Search for these keys being referenced in component code (not just defined in json)
rg "t\(['\"]" components/ app/ --type ts --type js 2>/dev/null | grep -E "(priceRange|noAlternatives|showPricing|hidePricing|localProduct)" || echo "No usage found in component code"Repository: KMS74/roshetta-ai
Length of output: 94
🏁 Script executed:
# Also search for direct key references (object notation style)
rg "Insights\.(priceRange|noAlternatives|showPricing|hidePricing|localProduct)" --max-count=10Repository: KMS74/roshetta-ai
Length of output: 43
Five Insights keys are unused and should be wired up or removed.
Verification confirms these translation keys are defined in both messages/en.json and messages/ar.json but have no usage in the codebase:
priceRange— the component renders price inline via{estimatedPrice.currency} {estimatedPrice.price}instead of using this templatenoAlternatives— no empty state UI renders when alternatives are unavailableshowPricing,hidePricing— the toggle button exists but lacks text labels from these keyslocalProduct— not referenced in the alternatives rendering
Either wire them up (e.g., use priceRange for consistent formatting, add noAlternatives as an empty state, drive toggle labels from showPricing/hidePricing) or remove them to prevent translation drift between locales.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@messages/en.json` around lines 67 - 79, The mentioned translation keys
(priceRange, noAlternatives, showPricing, hidePricing, localProduct) are defined
but unused; either wire them into the UI or remove them. Fix by: (1) replace the
inline price rendering with the i18n template key priceRange (pass a single
formatted price string or numeric price as the {price} param where the current
estimatedPrice rendering occurs), (2) add an empty-state render that uses
noAlternatives when the alternatives list is empty inside the alternatives
component, (3) make the pricing toggle button text come from showPricing and
hidePricing based on the toggle state instead of hardcoded text, and (4) use
localProduct as the label/aria-label when rendering each locally manufactured
alternative in the alternatives list; if you prefer not to surface these
strings, remove them from messages/en.json and messages/ar.json to avoid
translation drift.
| /// <reference types="next" /> | ||
| /// <reference types="next/image-types/global" /> | ||
| import "./.next/dev/types/routes.d.ts"; | ||
| import "./.next/types/routes.d.ts"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check Next.js version and recent changes to package files
echo "=== Current Next.js version ==="
cat package.json | jq '.dependencies.next // .devDependencies.next'
echo -e "\n=== Recent changes to package.json ==="
git log --oneline --follow -10 package.json
echo -e "\n=== Check if next-env.d.ts is in .gitignore ==="
if [ -f .gitignore ]; then
rg -n "next-env" .gitignore || echo "next-env.d.ts is NOT ignored"
fiRepository: KMS74/roshetta-ai
Length of output: 296
🏁 Script executed:
#!/bin/bash
# Check git history of next-env.d.ts
echo "=== Recent git log for next-env.d.ts ==="
git log --oneline -15 next-env.d.ts
echo -e "\n=== Latest commit details for next-env.d.ts ==="
git log -1 --format="%H %s" next-env.d.ts
LATEST_COMMIT=$(git log -1 --format="%H" next-env.d.ts)
echo -e "\n=== Check if next-env.d.ts change is in current PR (compare HEAD with main/master) ==="
git diff HEAD next-env.d.ts || echo "No diff found"
echo -e "\n=== Show the actual change in the latest commit ==="
git show $LATEST_COMMIT:next-env.d.ts | head -10
echo -e "\n=== Check .gitignore history ==="
git log --oneline -5 .gitignoreRepository: KMS74/roshetta-ai
Length of output: 834
🏁 Script executed:
#!/bin/bash
# Get detailed view of the change to next-env.d.ts in commit 8569471
echo "=== Full diff of next-env.d.ts in commit 8569471 ==="
git show 8569471 -- next-env.d.ts
echo -e "\n=== Check parent commit state ==="
PARENT_COMMIT=$(git rev-parse 8569471^)
echo "Parent commit: $PARENT_COMMIT"
git show $PARENT_COMMIT:next-env.d.ts 2>/dev/null || echo "File did not exist in parent commit"
echo -e "\n=== Check .gitignore changes in this commit ==="
git show 8569471 -- .gitignore | head -30Repository: KMS74/roshetta-ai
Length of output: 2764
Remove this auto-generated file from version control and add to .gitignore.
This is a Next.js auto-generated file that should never be committed to the repository. The file itself explicitly states "This file should not be edited" and is generated automatically by Next.js during builds. Committing it to version control will cause conflicts during Next.js version upgrades and violates Next.js best practices.
Required changes:
- Add
next-env.d.tsto.gitignore - Remove the file from git history:
git rm --cached next-env.d.ts
Additionally, this change is unrelated to the medication pricing feature described in the PR and should not be included in this commit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@next-env.d.ts` at line 3, The committed auto-generated Next.js type file
next-env.d.ts (containing the line `import "./.next/types/routes.d.ts";`) should
be removed from version control and ignored: add next-env.d.ts to .gitignore and
run git rm --cached next-env.d.ts to stop tracking it, then amend the commit so
this change is not part of the medication-pricing feature PR; ensure no code
edits are made to next-env.d.ts itself since it is auto-generated.
Summary by CodeRabbit
New Features
Documentation