feat: implement AI-powered requirement analysis with hidden internal pricing logic (#76)#81
feat: implement AI-powered requirement analysis with hidden internal pricing logic (#76)#81devin-ai-integration[bot] wants to merge 4 commits into
Conversation
…ic (#76) - Add Firebase Cloud Function (analyzeProject) that reads pricing config from Firestore and calls Gemini API server-side, returning only safe client-facing data (features, complexity, cost range, timeline, explanation) - Add ProjectEstimation dashboard page for clients to submit project descriptions and receive instant AI-generated cost estimates - Add PricingConfig admin page to manage feature prices, complexity multipliers, and estimation rules in Firestore - Add estimation history with expandable records - Store all pricing logic, prompts, and formulas exclusively server-side; API responses never leak internal calculation data - Add routes (/dashboard/estimation, /dashboard/pricing-config) and sidebar navigation entries - Update firebase.json with functions configuration Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rministic pricing - AI prompt now only performs feature extraction/classification with category names (no prices, multipliers, or formulas ever sent to AI) - Cost calculation, timeline estimation, and explanation generation all happen deterministically server-side in computeEstimate() - getPricingConfig() now merges Firestore data with defaults and validates numeric bounds to prevent partial/corrupt config - validateClassification() performs thorough type/enum validation of AI output before any downstream processing - No free-form AI text is returned to the client; explanation is built server-side from classification metadata Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…store index - getPricingConfig: validate all numeric fields, feature prices, and multipliers - validateClassification: reject categories not in allowed pricing set - Trim description server-side before validation - PricingConfig.tsx: remove hardcoded DEFAULT_CONFIG with pricing values; load purely from Firestore with access-denied handling - Add firestore.indexes.json with composite index for estimations query (userId ASC + createdAt DESC) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nt-side - Remove functions/ directory entirely (Cloud Functions require Blaze plan) - Install @google/generative-ai as frontend dependency - Move AI classification + deterministic cost computation to client-side estimationService with same two-phase architecture: Phase 1: Gemini classifies features (sees category names only, never prices) Phase 2: Client computes costs deterministically from pricing config - Pricing config read from Firestore with hardened validation and defaults - API key provided via VITE_GEMINI_API_KEY environment variable - Remove functions config from firebase.json, revert eslint ignores Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
📝 WalkthroughWalkthroughAdds two new dashboard features: an AI-powered project estimation page ( ChangesAI Estimation & Pricing Config Feature
Sequence Diagram(s)sequenceDiagram
actor User
participant ProjectEstimation
participant analyzeProject
participant GeminiAPI
participant Firestore
User->>ProjectEstimation: Submit project description
ProjectEstimation->>analyzeProject: analyzeProject(description, userId)
analyzeProject->>Firestore: getDoc(pricingConfig/default)
Firestore-->>analyzeProject: pricing rules (or defaults)
analyzeProject->>GeminiAPI: generateContent(classificationPrompt + description)
GeminiAPI-->>analyzeProject: JSON {projectType, overallComplexity, features, hasSignificantUnknowns}
analyzeProject->>analyzeProject: computeEstimate(classification, pricing)
analyzeProject->>Firestore: addDoc(estimations, {userId, description, result, createdAt})
analyzeProject-->>ProjectEstimation: EstimationResult
ProjectEstimation-->>User: Render cost/timeline/feature breakdown
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
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)
Comment |
|
Visit the preview URL for this PR (updated for commit c7e9ea0): https://servio-0--pr81-devin-1781985456-ai-05tfjrkc.web.app (expires Sat, 27 Jun 2026 19:59:05 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 15915abb5951eb298a844eda460b24f444d93a69 |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (2)
src/dashboard/services/estimationService.ts (2)
373-378: ⚡ Quick winFirestore write may fail silently if security rules deny access.
If Firestore rules reject the write for unauthenticated or unauthorized users, the error propagates but the user sees a generic failure. Consider validating
userIdis non-empty before attempting the write.🛡️ Proposed validation
+ if (!userId) { + throw new Error("You must be signed in to save estimation history."); + } + await addDoc(collection(db, "estimations"), { userId,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/dashboard/services/estimationService.ts` around lines 373 - 378, The addDoc call that writes to the "estimations" collection does not validate that userId is non-empty before attempting the write. Add a validation check before the addDoc call to ensure userId is populated and non-empty, and throw a descriptive error if validation fails. This prevents silent Firestore failures when security rules reject writes due to missing or invalid userId values.
217-218: ⚡ Quick winHardcoded fallback multiplier
1.3duplicates existing constant.This magic number should reference a named constant or use the "medium" multiplier from the pricing config to stay consistent if defaults change.
♻️ Suggested fix
- const multiplier = pricing.complexityMultipliers[f.complexity] ?? 1.3; + const multiplier = + pricing.complexityMultipliers[f.complexity] ?? + pricing.complexityMultipliers["medium"] ?? + 1.3;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/dashboard/services/estimationService.ts` around lines 217 - 218, The hardcoded fallback multiplier value 1.3 in the complexity multipliers lookup (in the line with pricing.complexityMultipliers[f.complexity] ?? 1.3) is a magic number that duplicates configuration logic. Replace this hardcoded 1.3 with either a named constant defined at the top of the file or by referencing an existing default multiplier from the pricing configuration object (such as the "medium" complexity level multiplier). This ensures the fallback behavior stays synchronized with the pricing config defaults if they change in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/dashboard/pages/PricingConfig.tsx`:
- Around line 40-52: The EMPTY_CONFIG constant contains values
(minimumProjectCost: 0, maximumProjectCost: 0) that could fail validation in the
estimationService.getPricingConfig() validation logic, causing silent fallback
to defaults when admins attempt to save partial changes. Add client-side
validation checks before the PricingConfig is saved to the service; these checks
should verify that the values meet the same constraints as those checked in
estimationService (such as ensuring minimumProjectCost and maximumProjectCost
have valid relationship), and display validation error messages to the user if
the config would be rejected, preventing the silent failure scenario.
- Around line 376-383: The input field for the multiplier has min={0}, which
allows invalid values that would silently fall back to defaults since
estimationService.getPricingConfig() requires multiplier > 0. Change the min
attribute from 0 to 0.1 in the number input element that calls updateMultiplier
in its onChange handler to enforce the valid range at the UI level.
- Around line 469-480: The riskFactorMultiplier input field in PricingConfig.tsx
has a minimum value of 0, but the backend validation in estimationService.ts
requires the value to be greater than 0 (values <= 0 trigger fallback to
defaults). Change the min attribute on the number input from 0 to a value
greater than 0 (such as 0.05 to match the existing step value) to prevent users
from entering invalid values that will be rejected by the backend.
- Around line 61-105: Add a proactive admin role check in the PricingConfig
component to verify the authenticated user has admin privileges before rendering
the form and attempting data fetch. Retrieve the current authenticated user and
check their admin status/claim using a utility function, then use this check to
conditionally set the accessDenied state early (before or alongside the
loadConfig useEffect). This prevents any authenticated non-admin user from
seeing the pricing config form UI and making Firestore requests, rather than
relying solely on catching permission errors after the fact in the catch block
of loadConfig.
In `@src/dashboard/pages/ProjectEstimation.tsx`:
- Around line 294-297: The analyzeProject function call on line 296 uses an
empty string fallback for userId when currentUser is absent, which creates
orphaned records. Before calling analyzeProject in the estimation submission
flow, add a validation check to ensure currentUser?.uid exists and is non-empty,
and prevent form submission if the user ID is not available. Apply the same
validation fix to the similar code referenced on line 403.
- Around line 343-351: The History button with the handleLoadHistory onClick
handler is hidden on small screens due to the `hidden sm:flex` Tailwind classes
in the className prop, making it inaccessible to mobile users. Remove the
`hidden sm:flex` classes from the Button component so that the History button is
visible and accessible on all viewport sizes, including mobile.
In `@src/dashboard/services/estimationService.ts`:
- Around line 359-365: The code in the JSON parsing logic in
estimationService.ts does not handle empty responses from the Gemini model
before attempting to parse. After calling result.response.text() to get the
responseText, add a guard check to validate that responseText is not empty or
whitespace before attempting JSON.parse(). If the response is empty, throw a
descriptive error immediately indicating the AI returned an empty response,
which helps distinguish this case from actual JSON parsing errors in the catch
block.
- Around line 327-332: The Gemini API key is currently being exposed in the
client-side bundle by being used directly with the GoogleGenerativeAI client
library in the estimationService. Instead of making direct calls to the Gemini
API from the browser, refactor the code to call a backend endpoint that you will
create. Remove the VITE_GEMINI_API_KEY retrieval and GoogleGenerativeAI client
instantiation from estimationService, and replace those calls with HTTP requests
to a new backend proxy endpoint. The backend endpoint should securely store the
VITE_GEMINI_API_KEY on the server side and handle all communication with the
Gemini API, returning only the necessary results to the frontend. This ensures
the API key never reaches the browser.
---
Nitpick comments:
In `@src/dashboard/services/estimationService.ts`:
- Around line 373-378: The addDoc call that writes to the "estimations"
collection does not validate that userId is non-empty before attempting the
write. Add a validation check before the addDoc call to ensure userId is
populated and non-empty, and throw a descriptive error if validation fails. This
prevents silent Firestore failures when security rules reject writes due to
missing or invalid userId values.
- Around line 217-218: The hardcoded fallback multiplier value 1.3 in the
complexity multipliers lookup (in the line with
pricing.complexityMultipliers[f.complexity] ?? 1.3) is a magic number that
duplicates configuration logic. Replace this hardcoded 1.3 with either a named
constant defined at the top of the file or by referencing an existing default
multiplier from the pricing configuration object (such as the "medium"
complexity level multiplier). This ensures the fallback behavior stays
synchronized with the pricing config defaults if they change in the future.
🪄 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 Plus
Run ID: 306d5e0d-d61c-42c0-adaa-a345c94db083
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
firebase.jsonfirestore.indexes.jsonpackage.jsonsrc/app/App.tsxsrc/dashboard/components/DashboardLayout.tsxsrc/dashboard/pages/PricingConfig.tsxsrc/dashboard/pages/ProjectEstimation.tsxsrc/dashboard/services/estimationService.tssrc/dashboard/types.ts
| const EMPTY_CONFIG: PricingConfig = { | ||
| featurePricing: {}, | ||
| complexityMultipliers: { | ||
| low: 1.0, | ||
| medium: 1.3, | ||
| high: 1.7, | ||
| enterprise: 2.2, | ||
| }, | ||
| minimumProjectCost: 0, | ||
| maximumProjectCost: 0, | ||
| bufferPercentage: 0, | ||
| riskFactorMultiplier: 1.0, | ||
| }; |
There was a problem hiding this comment.
EMPTY_CONFIG values will fail validation in estimationService.getPricingConfig().
minimumProjectCost: 0, maximumProjectCost: 0, and riskFactorMultiplier: 1.0 (though valid) create a state where maximumProjectCost < minimumProjectCost is false only because both are 0. However, if an admin saves partial changes, the validation at lines 107-120 in estimationService.ts may reject the config and silently fall back to defaults, making admin edits appear to have no effect.
Consider adding client-side validation warnings before save.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/PricingConfig.tsx` around lines 40 - 52, The EMPTY_CONFIG
constant contains values (minimumProjectCost: 0, maximumProjectCost: 0) that
could fail validation in the estimationService.getPricingConfig() validation
logic, causing silent fallback to defaults when admins attempt to save partial
changes. Add client-side validation checks before the PricingConfig is saved to
the service; these checks should verify that the values meet the same
constraints as those checked in estimationService (such as ensuring
minimumProjectCost and maximumProjectCost have valid relationship), and display
validation error messages to the user if the config would be rejected,
preventing the silent failure scenario.
| export function PricingConfig() { | ||
| const [config, setConfig] = useState<PricingConfig | null>(null); | ||
| const [loading, setLoading] = useState(true); | ||
| const [saving, setSaving] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [success, setSuccess] = useState(false); | ||
| const [accessDenied, setAccessDenied] = useState(false); | ||
| const [newFeatureKey, setNewFeatureKey] = useState(""); | ||
| const [newFeaturePrice, setNewFeaturePrice] = useState(""); | ||
|
|
||
| useEffect(() => { | ||
| async function loadConfig() { | ||
| try { | ||
| const snap = await getDoc(doc(db, "pricingConfig", "default")); | ||
| if (snap.exists()) { | ||
| const raw = snap.data() as Partial<PricingConfig>; | ||
| setConfig({ | ||
| featurePricing: raw.featurePricing ?? {}, | ||
| complexityMultipliers: raw.complexityMultipliers ?? { | ||
| low: 1.0, | ||
| medium: 1.3, | ||
| high: 1.7, | ||
| enterprise: 2.2, | ||
| }, | ||
| minimumProjectCost: raw.minimumProjectCost ?? 0, | ||
| maximumProjectCost: raw.maximumProjectCost ?? 0, | ||
| bufferPercentage: raw.bufferPercentage ?? 0, | ||
| riskFactorMultiplier: raw.riskFactorMultiplier ?? 1.0, | ||
| }); | ||
| } else { | ||
| setConfig(EMPTY_CONFIG); | ||
| } | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : ""; | ||
| if (message.includes("permission") || message.includes("PERMISSION_DENIED")) { | ||
| setAccessDenied(true); | ||
| } else { | ||
| setConfig(EMPTY_CONFIG); | ||
| } | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| loadConfig(); | ||
| }, []); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for Firestore security rules file
fd -e rules -i firestore
cat firestore.rules 2>/dev/null || echo "No firestore.rules found in repo root"Repository: hrx01-dev/Servio
Length of output: 98
🏁 Script executed:
# Search for firestore rules files throughout the entire repo
fd -type f -name '*.rules' -o -name '*firestore*' -o -name '*security*' | grep -i ruleRepository: hrx01-dev/Servio
Length of output: 231
🏁 Script executed:
# Check the directory structure to understand the project layout
git ls-files | head -30Repository: hrx01-dev/Servio
Length of output: 743
🏁 Script executed:
# Look for authentication/authorization related files and utilities
fd -type f \( -name '*auth*' -o -name '*permission*' -o -name '*role*' \) | grep -E '\.(ts|tsx|js|json)$'Repository: hrx01-dev/Servio
Length of output: 231
🏁 Script executed:
# Check routing configuration to see if there are route guards
fd -type f -name '*route*' -o -name '*router*' | grep -E '\.(ts|tsx|js)$'Repository: hrx01-dev/Servio
Length of output: 231
🏁 Script executed:
# Examine the PricingConfig component more thoroughly
cat src/dashboard/pages/PricingConfig.tsxRepository: hrx01-dev/Servio
Length of output: 16563
🏁 Script executed:
# Search for files with .rules extension
find . -name "*.rules" -type f 2>/dev/nullRepository: hrx01-dev/Servio
Length of output: 42
🏁 Script executed:
# Check firebase.json for rules configuration
cat firebase.json 2>/dev/null | head -50Repository: hrx01-dev/Servio
Length of output: 781
🏁 Script executed:
# Search for route protection/guards and admin checks
rg "admin|guard|protect|role|permission|claim" --type ts --type tsx -i | head -40Repository: hrx01-dev/Servio
Length of output: 88
🏁 Script executed:
# Look for the route definition of pricing-config
rg "pricing-config" --type ts --type tsxRepository: hrx01-dev/Servio
Length of output: 88
🏁 Script executed:
# Check if there's any authentication wrapper or context usage in relevant components
rg "PricingConfig|pricing" -A 3 -B 3 --type ts --type tsx | head -50Repository: hrx01-dev/Servio
Length of output: 88
🏁 Script executed:
# Search for pricing-config route and admin patterns without type filter
rg "pricing-config|admin|role|permission" -A 2 -B 2 | head -60Repository: hrx01-dev/Servio
Length of output: 2962
🏁 Script executed:
# Check how PricingConfig is imported/used in the app routing
rg "PricingConfig" -A 3 -B 3Repository: hrx01-dev/Servio
Length of output: 5987
🏁 Script executed:
# Look for useAuth or auth context usage in components to understand auth pattern
rg "useAuth|AuthContext" --max-count 20Repository: hrx01-dev/Servio
Length of output: 1681
🏁 Script executed:
# Check the App.tsx or routing setup to see if there's a guard on the pricing route
cat src/app/App.tsx | head -150Repository: hrx01-dev/Servio
Length of output: 6163
🏁 Script executed:
# Check ProtectedRoute to see what it actually protects
cat src/dashboard/components/ProtectedRoute.tsxRepository: hrx01-dev/Servio
Length of output: 858
🏁 Script executed:
# Check useAuth hook to see what claims/data it provides
cat src/Firebase/useAuth.tsRepository: hrx01-dev/Servio
Length of output: 212
🏁 Script executed:
# Check AuthContextObject for the user type definition
cat src/Firebase/AuthContextObject.tsRepository: hrx01-dev/Servio
Length of output: 324
🏁 Script executed:
# Look for any admin claim checks or custom claims in the codebase
rg "admin|claim|custom" | grep -i userRepository: hrx01-dev/Servio
Length of output: 42
Add client-side admin role check before rendering pricing config UI.
The /dashboard/pricing-config route is protected by ProtectedRoute, which requires authentication but does NOT verify admin privileges. Any authenticated user can render the component and its form, even though PricingConfig.tsx catches Firestore permission errors after the fact. While Firestore rules likely restrict actual read/write access (permission error handling is present), the component lacks proactive authorization checks.
Consider:
- Adding a role/claim utility function to check for admin status from the authenticated user
- Gating the component render with an admin check before attempting data fetch, rather than relying solely on Firestore permission errors
- Note: Firestore security rules file not found in repository; if properly configured server-side, actual data access is protected
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/PricingConfig.tsx` around lines 61 - 105, Add a proactive
admin role check in the PricingConfig component to verify the authenticated user
has admin privileges before rendering the form and attempting data fetch.
Retrieve the current authenticated user and check their admin status/claim using
a utility function, then use this check to conditionally set the accessDenied
state early (before or alongside the loadConfig useEffect). This prevents any
authenticated non-admin user from seeing the pricing config form UI and making
Firestore requests, rather than relying solely on catching permission errors
after the fact in the catch block of loadConfig.
| <input | ||
| type="number" | ||
| min={0} | ||
| step={0.1} | ||
| value={value} | ||
| onChange={(e) => | ||
| updateMultiplier(key, parseFloat(e.target.value) || 0) | ||
| } |
There was a problem hiding this comment.
Multiplier min={0} allows invalid values that break estimation.
estimationService.getPricingConfig() requires multiplier > 0 (line 105). Setting a multiplier to 0 here will cause silent fallback to defaults. Use min={0.1} or add validation feedback.
🛡️ Proposed fix
<input
type="number"
- min={0}
+ min={0.1}
step={0.1}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/PricingConfig.tsx` around lines 376 - 383, The input
field for the multiplier has min={0}, which allows invalid values that would
silently fall back to defaults since estimationService.getPricingConfig()
requires multiplier > 0. Change the min attribute from 0 to 0.1 in the number
input element that calls updateMultiplier in its onChange handler to enforce the
valid range at the UI level.
| <input | ||
| type="number" | ||
| min={0} | ||
| step={0.05} | ||
| value={config.riskFactorMultiplier} | ||
| onChange={(e) => | ||
| setConfig((prev) => { | ||
| if (!prev) return prev; | ||
| return { | ||
| ...prev, | ||
| riskFactorMultiplier: parseFloat(e.target.value) || 0, | ||
| }; |
There was a problem hiding this comment.
Risk factor multiplier allows 0, which fails backend validation.
Same issue as complexity multipliers—riskFactorMultiplier <= 0 triggers fallback to defaults per estimationService.ts line 115.
🛡️ Proposed fix
<input
type="number"
- min={0}
+ min={0.01}
step={0.05}📝 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.
| <input | |
| type="number" | |
| min={0} | |
| step={0.05} | |
| value={config.riskFactorMultiplier} | |
| onChange={(e) => | |
| setConfig((prev) => { | |
| if (!prev) return prev; | |
| return { | |
| ...prev, | |
| riskFactorMultiplier: parseFloat(e.target.value) || 0, | |
| }; | |
| <input | |
| type="number" | |
| min={0.01} | |
| step={0.05} | |
| value={config.riskFactorMultiplier} | |
| onChange={(e) => | |
| setConfig((prev) => { | |
| if (!prev) return prev; | |
| return { | |
| ...prev, | |
| riskFactorMultiplier: parseFloat(e.target.value) || 0, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/PricingConfig.tsx` around lines 469 - 480, The
riskFactorMultiplier input field in PricingConfig.tsx has a minimum value of 0,
but the backend validation in estimationService.ts requires the value to be
greater than 0 (values <= 0 trigger fallback to defaults). Change the min
attribute on the number input from 0 to a value greater than 0 (such as 0.05 to
match the existing step value) to prevent users from entering invalid values
that will be rejected by the backend.
| const estimation = await analyzeProject( | ||
| description.trim(), | ||
| currentUser?.uid ?? "", | ||
| ); |
There was a problem hiding this comment.
Avoid submitting with an empty userId fallback.
Line 296 falls back to "" when currentUser is absent. That can cause failed writes or inconsistent/orphaned estimation records. Block submit until currentUser.uid exists.
Proposed fix
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+ if (!currentUser?.uid) {
+ setError("You must be signed in to create an estimate.");
+ return;
+ }
if (!description.trim() || description.trim().length < 10) {
setError("Please provide a detailed project description (at least 10 characters).");
return;
}
@@
const estimation = await analyzeProject(
description.trim(),
- currentUser?.uid ?? "",
+ currentUser.uid,
);
@@
- disabled={loading || !description.trim()}
+ disabled={loading || !description.trim() || !currentUser?.uid}Also applies to: 403-403
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/ProjectEstimation.tsx` around lines 294 - 297, The
analyzeProject function call on line 296 uses an empty string fallback for
userId when currentUser is absent, which creates orphaned records. Before
calling analyzeProject in the estimation submission flow, add a validation check
to ensure currentUser?.uid exists and is non-empty, and prevent form submission
if the user ID is not available. Apply the same validation fix to the similar
code referenced on line 403.
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={handleLoadHistory} | ||
| className="hidden sm:flex items-center gap-2" | ||
| > | ||
| <History className="h-4 w-4" /> | ||
| History | ||
| </Button> |
There was a problem hiding this comment.
History is inaccessible on mobile viewports.
Line 347 hides the only “History” trigger on small screens (hidden sm:flex), so users cannot open previous estimates on mobile.
Proposed fix
- <Button
+ <Button
variant="outline"
size="sm"
onClick={handleLoadHistory}
- className="hidden sm:flex items-center gap-2"
+ className="flex items-center gap-2"
>📝 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.
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleLoadHistory} | |
| className="hidden sm:flex items-center gap-2" | |
| > | |
| <History className="h-4 w-4" /> | |
| History | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={handleLoadHistory} | |
| className="flex items-center gap-2" | |
| > | |
| <History className="h-4 w-4" /> | |
| History | |
| </Button> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/pages/ProjectEstimation.tsx` around lines 343 - 351, The
History button with the handleLoadHistory onClick handler is hidden on small
screens due to the `hidden sm:flex` Tailwind classes in the className prop,
making it inaccessible to mobile users. Remove the `hidden sm:flex` classes from
the Button component so that the History button is visible and accessible on all
viewport sizes, including mobile.
| const apiKey = import.meta.env.VITE_GEMINI_API_KEY as string | undefined; | ||
| if (!apiKey) { | ||
| throw new Error( | ||
| "AI service is not configured. Set VITE_GEMINI_API_KEY in your environment.", | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
How to restrict Google AI API key by HTTP referrer
💡 Result:
To restrict a Google AI (Gemini) API key by HTTP referrer, you must use the Google Cloud Console or the gcloud CLI to apply browser-based application restrictions [1][2][3]. Note that you can typically only apply one type of application restriction (e.g., HTTP referrers, IP addresses, Android apps, or iOS apps) to a single API key [4][5][6]. Using Google Cloud Console: 1. Navigate to the Google Cloud Console Credentials page [1][3]. 2. Select the project and click on the name of the API key you wish to restrict [1][3]. 3. Under the Application restrictions section, select HTTP referrers (websites) [2][4]. 4. Add the allowed URLs (e.g., example.com/* or .example.com/) to the allowed referrers list [2]. 5. Click Save to apply the restrictions [1][3]. Using gcloud CLI: You can update an existing key with the --allowed-referrers flag [7]: gcloud services api-keys update projects/PROJECT_ID/locations/global/keys/KEY_ID --allowed-referrers="https://www.example.com/*,http://*.example.com/" When creating a new key, you can similarly specify the flag [8][9]: gcloud services api-keys create --allowed-referrers="https://www.example.com/" Important Considerations: - API Permissions: You must have the apikeys.keys.update permission on the Google Cloud project to modify these settings [1][3]. - Security: Restricting your key is a security best practice that limits the potential damage if the key is exposed [4][3]. - Separate Keys: If you need to use the key for different environments or client types (e.g., both a web app and a backend server), create separate, restricted API keys for each [4].
Citations:
- 1: https://ai.google.dev/gemini-api/docs/api-key
- 2: https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys
- 3: https://ai.google.dev/gemini-api/docs/interactions/api-key
- 4: https://cloud.google.com/blog/topics/developers-practitioners/api-keys-are-open-secrets
- 5: https://docs.cloud.google.com/api-keys/docs/reference/rest/v2/projects.locations.keys
- 6: https://cloud.google.com/api-keys/docs/reference/rest/v2/projects.locations.keys
- 7: https://cloud.google.com/sdk/gcloud/reference/services/api-keys/update
- 8: https://cloud.google.com/sdk/gcloud/reference/alpha/services/api-keys/create
- 9: https://docs.cloud.google.com/sdk/gcloud/reference/services/api-keys/create
🏁 Script executed:
# First, let's look at the file structure and the specific lines mentioned
head -350 src/dashboard/services/estimationService.ts | tail -50Repository: hrx01-dev/Servio
Length of output: 1524
🏁 Script executed:
# Search for how the apiKey is used after it's retrieved
rg "apiKey" src/dashboard/services/estimationService.ts -A 5 -B 2Repository: hrx01-dev/Servio
Length of output: 549
🏁 Script executed:
# Check if there are any server-side Gemini API implementations
fd -e ts -e js | xargs rg "GEMINI" 2>/dev/null | grep -v node_modulesRepository: hrx01-dev/Servio
Length of output: 308
🏁 Script executed:
# Check if there's any server-side implementation or backend API
fd -e ts -e js | xargs rg "generateContent|GoogleGenerativeAI" 2>/dev/null | head -20Repository: hrx01-dev/Servio
Length of output: 347
🏁 Script executed:
# Look for environment configuration and build setup
cat vite.config.ts 2>/dev/null || cat vite.config.js 2>/dev/null || echo "No vite config found in root"Repository: hrx01-dev/Servio
Length of output: 992
🌐 Web query:
Gemini API client-side usage security best practices Google recommendations
💡 Result:
Google provides strict recommendations for securing Gemini API usage, emphasizing that API keys should never be exposed in client-side code [1][2][3]. Hardcoding or embedding keys in web or mobile applications makes them extractable by users, leading to potential unauthorized access, quota exhaustion, and unexpected billing charges [1][4][5]. Core Best Practices 1. Use a Backend Proxy: For production applications, implement a backend proxy server to handle API calls [1][2]. The frontend should communicate with your own backend, which then securely adds the API credentials and interacts with the Gemini API [4][5]. This ensures your API keys remain on the server and are never exposed to the client [1][6][7]. 2. Implement Firebase App Check: If you are building mobile or web apps that require client-side interaction, Google strongly recommends using Vertex AI in Firebase SDKs combined with Firebase App Check [6][8]. App Check verifies that incoming API requests are originating from your legitimate, authorized application, effectively preventing abuse by unauthorized clients [9][10]. 3. Enforce API and Application Restrictions: Always apply strict restrictions to your API keys in the Google Cloud Console [11][4]. - API Restrictions: Limit the key to only the specific services it requires (e.g., Gemini API) [11]. - Application Restrictions: Restrict the key to specific websites or IP addresses to prevent it from being used elsewhere [11]. 4. Proper Secret Management: - Never commit API keys to version control systems like Git [1][2]. - Use secure storage services like Google Cloud Secret Manager to manage your keys [1][11]. - Utilize environment variables in your server-side environment rather than configuration files [1][2]. 5. Monitor and Protect Usage: - Set up billing alerts in the Google Cloud Console to get notified of usage spikes [1][2]. - Periodically rotate your API keys by creating new ones and deleting old, unused credentials [4]. If you are currently exposing keys in your client-side code, Google recommends deleting the compromised keys immediately via Google AI Studio or the Google Cloud Console, issuing new keys, and migrating to a backend proxy or a Firebase-based architecture [6][8][5].
Citations:
- 1: https://ai.google.dev/gemini-api/docs/interactions/api-key
- 2: https://ai.google.dev/gemini-api/docs/api-key
- 3: https://googleapis.github.io/js-genai/release%5Fdocs/index.html
- 4: https://docs.cloud.google.com/docs/authentication/api-keys-best-practices
- 5: https://gemilab.net/en/articles/gemini-api/gemini-api-key-safe-operations-indie-developer-checklist
- 6: https://firebase.google.com/docs/ai-logic/migrate-from-google-ai-client-sdks
- 7: https://glaforge.dev/posts/2026/02/09/decoded-how-google-ai-studio-securely-proxies-gemini-api-requests/
- 8: https://firebase.google.com/docs/vertex-ai/migrate-to-vertex-ai
- 9: https://firebase.google.com/docs/ai-logic/app-check
- 10: https://firebase.google.com/docs/vertex-ai/app-check
- 11: https://cloud.google.com/blog/topics/developers-practitioners/api-keys-are-open-secrets
Move Gemini API calls to a backend proxy to secure the API key.
The VITE_GEMINI_API_KEY is exposed in the client-side bundle because it's instantiated with the GoogleGenerativeAI client library in the browser. Google's official guidance explicitly recommends using a backend proxy as the primary solution—have the frontend call your own backend, which securely adds credentials and communicates with Gemini API. HTTP referrer restrictions and usage quotas are helpful secondary measures but do not eliminate the core risk of key extraction. A backend proxy prevents the key from ever reaching the browser.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/services/estimationService.ts` around lines 327 - 332, The
Gemini API key is currently being exposed in the client-side bundle by being
used directly with the GoogleGenerativeAI client library in the
estimationService. Instead of making direct calls to the Gemini API from the
browser, refactor the code to call a backend endpoint that you will create.
Remove the VITE_GEMINI_API_KEY retrieval and GoogleGenerativeAI client
instantiation from estimationService, and replace those calls with HTTP requests
to a new backend proxy endpoint. The backend endpoint should securely store the
VITE_GEMINI_API_KEY on the server side and handle all communication with the
Gemini API, returning only the necessary results to the frontend. This ensures
the API key never reaches the browser.
| const responseText = result.response.text(); | ||
| let parsed: unknown; | ||
| try { | ||
| parsed = JSON.parse(responseText); | ||
| } catch { | ||
| throw new Error("Failed to parse AI response. Please try again."); | ||
| } |
There was a problem hiding this comment.
Empty or malformed AI response can cause unhandled exception.
If the Gemini model returns an empty response (e.g., content filtered, rate limited), result.response.text() may return an empty string causing JSON.parse("") to throw a SyntaxError with a less informative message than the current catch provides.
🛡️ Proposed guard for empty response
const responseText = result.response.text();
+ if (!responseText || responseText.trim().length === 0) {
+ throw new Error("AI returned an empty response. Please try again.");
+ }
let parsed: unknown;
try {
parsed = JSON.parse(responseText);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/dashboard/services/estimationService.ts` around lines 359 - 365, The code
in the JSON parsing logic in estimationService.ts does not handle empty
responses from the Gemini model before attempting to parse. After calling
result.response.text() to get the responseText, add a guard check to validate
that responseText is not empty or whitespace before attempting JSON.parse(). If
the response is empty, throw a descriptive error immediately indicating the AI
returned an empty response, which helps distinguish this case from actual JSON
parsing errors in the catch block.
|
Superseded — final fixes applied. |
Summary
Adds an AI-powered project estimation system where clients enter a natural-language project description and receive an instant cost/timeline estimate. Supersedes #77, #79, #80 — restructured per review feedback to remove Firebase Cloud Functions (which require the paid Blaze plan) and run everything client-side.
Architecture (two-phase design, no Cloud Functions)
The AI prompt receives only feature category names for classification. All cost/timeline/explanation generation is deterministic code in
computeEstimate(). The Gemini API key is provided viaVITE_GEMINI_API_KEYenv var.What's new
src/dashboard/services/estimationService.ts— Client-side estimation engine:getPricingConfig()reads FirestorepricingConfig/default, merges with defaults, validates all numeric fieldsbuildClassificationPrompt()sends only category names to Gemini (no prices)validateClassification(data, allowedCategories)— strict type/enum/category validationcomputeEstimate()— deterministic cost, timeline, and explanation generationanalyzeProject(description, userId)— orchestrates the full flowfetchEstimationHistory(uid)— queries user's past estimationssrc/dashboard/pages/ProjectEstimation.tsx— Client estimation page:src/dashboard/pages/PricingConfig.tsx— Admin pricing config page:pricingConfig/defaultin Firestorefirestore.indexes.json— Composite index forestimationsquery (userIdASC +createdAtDESC)Setup
VITE_GEMINI_API_KEY=<your-key>to.envfirebase deploy --only firestore:indexespricingConfigto admin-only accessCloses #76
Link to Devin session: https://app.devin.ai/sessions/c46ef0652e464fe8b81fbb3cc5147eb3
Requested by: @hrx01-dev
Summary by CodeRabbit
Release Notes