Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an AI Assistant feature (client chat UI, client aiService, Express backend, and Firebase function using Anthropic), navigation and type updates, email verification/resend flows, chart/timeline rendering improvements, mobile UX & security docs, and server/functions scaffolding with env/config updates. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as AIAssistant Component
participant Client as aiService (client)
participant Firebase as Firebase Auth
participant Server as Backend (Express)
participant Function as Firebase Function
participant Claude as Anthropic Claude
User->>UI: types message & send
UI->>Client: sendMessage(messages, timeBlocks)
Client->>Firebase: getAuthToken()
Client->>Server: POST /api/ai/message (Authorization: Bearer)
alt Server proxies to cloud function
Server->>Function: invoke sendAIMessage
Function->>Claude: request with system prompt + context
Claude-->>Function: response (may include JSON schedule)
Function-->>Server: { message, timeBlocks? }
else Server calls Claude directly
Server->>Claude: request with system prompt + context
Claude-->>Server: response (may include JSON schedule)
end
Server->>Server: parse schedule JSON, strip block from message
Server-->>Client: { message, timeBlocks? }
Client-->>UI: AIScheduleResponse
UI->>UI: render assistant message and schedule preview
alt user applies schedule
UI->>Dashboard: onApplySchedule(newBlocks)
Dashboard->>Firebase: save schedule (if authenticated)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 11
🧹 Nitpick comments (2)
src/components/AuthButtons.tsx (2)
183-184: MoveuseStatehooks to the top of the component.
isResendingVerificationandverificationMessagestate are declared on lines 183–184, after theGoogleIconinner component definition on lines 174–181. All hooks should be declared at the top of the component body before any inner component definitions to maintain conventional hook ordering and readability.♻️ Suggested refactor
// At the top of AuthButtons(), with the other useState declarations (lines 12-19): + const [isResendingVerification, setIsResendingVerification] = useState(false); + const [verificationMessage, setVerificationMessage] = useState(''); // ... const GoogleIcon = () => ( ... ); - const [isResendingVerification, setIsResendingVerification] = useState(false); - const [verificationMessage, setVerificationMessage] = useState('');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AuthButtons.tsx` around lines 183 - 184, Move the two useState declarations (isResendingVerification and verificationMessage) to the top of the AuthButtons component body, before any inner component/constant definitions (e.g., the GoogleIcon inner component), so all hooks are declared in hook order at the start of the component; locate the lines where useState is called for isResendingVerification and verificationMessage and cut/paste them above the GoogleIcon definition (and any other inner components) to ensure hooks run consistently.
186-200:setTimeoutcallbacks inhandleResendVerificationare not cleaned up.Both
setTimeout(() => setVerificationMessage(''), 5000)calls (lines 193 and 196) hold closures over the setter. If the component unmounts before 5 s elapses, these fire against a stale closure. React 18+/19 suppresses the resulting no-op, but it is cleaner to track the timer IDs and clear them (e.g., in auseEffectcleanup or auseRef).This is low-risk in practice but worth addressing for correctness hygiene.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AuthButtons.tsx` around lines 186 - 200, The two setTimeout calls in handleResendVerification create timers that can run after unmount; fix by storing their IDs in a ref (e.g., resendTimerRef) and clearing any existing timer before starting a new one, use the ref to clearTimeout in a useEffect cleanup to avoid stale-setState, and update handleResendVerification to set the ref when scheduling the clearing of setVerificationMessage; keep existing state setters (setIsResendingVerification, setVerificationMessage) and the resend call (resendVerificationEmail) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.env.example:
- Around line 12-14: The Claude API key is currently exposed as
VITE_CLAUDE_API_KEY and used directly from the client (see
src/services/aiService.ts line ~119); move the key to a server-only env var
(e.g., CLAUDE_API_KEY in .env) and stop using VITE_CLAUDE_API_KEY. Implement a
server-side endpoint (e.g., POST /api/claude or a serverless function handler
such as handleClaudeProxy) that reads process.env.CLAUDE_API_KEY and forwards
client requests to Claude, then update the client code in aiService.ts to call
that endpoint instead of calling Claude directly or reading VITE_CLAUDE_API_KEY;
ensure server-side validation/logging and remove the client-side env reference
from .env.example.
In `@MOBILE_SUPPORT.md`:
- Around line 48-56: The fenced code block(s) in MOBILE_SUPPORT.md are missing
language identifiers (MD040); update each triple-backtick block shown (the block
containing the bullet list "Calculate chart size based on viewport width" etc.)
and any other fenced blocks in the file by adding an appropriate language tag
(e.g., ```text, ```css, ```html or ```typescript) after the opening backticks so
markdownlint passes and code blocks render with consistent language metadata.
In `@MOBILE_UX_IMPROVEMENTS.md`:
- Around line 41-43: The fenced code block containing "[ Save Schedule ] [
Delete All ]" is missing a language identifier (MD040); update the opening fence
from ``` to ```text so the snippet is explicitly marked as plain text,
preserving the content and preventing the MD040 warning.
In `@SECURITY_TODOS.md`:
- Around line 10-27: Consolidate and tighten the Firestore rules for schedules:
remove the duplicate allow create rules and replace them with a single allow
create that validates request.auth != null, request.resource.data.userId ==
request.auth.uid, and enforces the scheduleCount limit via
get(...users/$(request.auth.uid)).data.scheduleCount < 10 (use
request.resource.data.* because resource is undefined on create); keep read as
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
and split write into allow update, delete: if request.auth != null &&
resource.data.userId == request.auth.uid && request.resource.data.userId ==
request.auth.uid to prevent changing userId on updates.
In `@src/auth.ts`:
- Around line 4-9: The signUp function currently allows sendEmailVerification
failures to bubble up and make the whole signup appear to fail; change signUp
(the async function using createUserWithEmailAndPassword and
sendEmailVerification) to catch errors from sendEmailVerification only (wrap the
sendEmailVerification(userCredential.user) call in a try/catch), log or report
the verification-send error (but do not rethrow), and always return the
successfully created userCredential so callers (e.g., AuthButtons.tsx) see the
signup as succeeded even if the verification email fails to send.
In `@src/components/AIAssistant.tsx`:
- Around line 104-195: The chatHistory built in handleSend currently maps all
messages (DisplayMessage) including locally injected assistant messages like
confirmation/error notices; tag local/system messages (e.g., add a boolean flag
like isLocal on DisplayMessage or use a special role value) and update
handleSend to filter out those local/system entries before mapping to
ChatMessage sent to sendMessage, ensuring only user/assistant model-originated
content is included (filtering by the tag or by role) and keep the existing
skip-greeting logic.
In `@src/components/AuthButtons.tsx`:
- Around line 202-207: handleReloadUser currently calls user?.reload() without a
.catch() and then sets the same user object (auth.currentUser) so React won't
re-render; update handleReloadUser to attach a .catch() that logs or surfaces
errors and, on success, increment a lightweight local state "reloadVersion"
(useState number) or toggle a boolean to force a re-render instead of calling
setUser with the same object; keep the existing setUser(auth.currentUser) if you
want but ensure you also bump reloadVersion so the UI (e.g., amber banner)
updates, and reference user.reload(), auth.currentUser, setUser and the new
reloadVersion state in the fix.
In `@src/components/Dashboard.tsx`:
- Around line 641-649: The AI-assistant callback currently applies blocks
directly; instead, in the onApplySchedule passed to AIAssistant validate each
incoming block with validateTimeBlock, drop or collect invalid ones, then pass
the filtered list through sortTimeBlocks before calling
setTimeBlocks(newBlocksSorted); reference the AIAssistant prop onApplySchedule,
the validateTimeBlock function, sortTimeBlocks helper, and the setTimeBlocks
state setter when making this change so AI-generated blocks follow the same
validation/sorting rules as manual blocks.
In `@src/components/Timeline.tsx`:
- Around line 225-240: The minHeight of 20px makes touch targets too small and
the text-fitting booleans (pixelHeight, isVeryShort, isShort, isMedium) still
use raw duration-based pixelHeight, so short blocks get compact layouts even
when minHeight inflates their visual size; change the block render to use a 44px
minimum touch target (style minHeight: '44px') and adjust the sizing logic:
compute a renderedPixelHeight = Math.max(pixelHeight, 44) (or convert minHeight
to the same coordinate system) and base isVeryShort/isShort/isMedium on
renderedPixelHeight instead of pixelHeight so label/layout decisions reflect the
actual rendered height on touch devices while keeping the minHeight enforced on
the div.
In `@src/services/aiService.ts`:
- Around line 45-60: parseScheduleFromResponse currently trusts the AI JSON and
creates TimeBlock IDs immediately; add a validation/sanitization pass before
mapping to TimeBlock: parse the JSON as now, then filter into a sanitized array
(e.g., sanitized) that only keeps objects with required properties (startTime,
endTime, label) where startTime/endTime match /^(\d{2}):(\d{2})$/ and hours
0–23, minutes 0–59 and minutes % 5 === 0, and optionally ensure startTime <
endTime and no overlapping blocks; if sanitized is empty return null, then map
sanitized to TimeBlock objects (id via crypto.randomUUID(), order = index,
default color '#60a5fa'), leaving parseScheduleFromResponse and TimeBlock shape
intact.
- Around line 3-5: The client currently exposes CLAUDE_API_KEY and calls
Anthropic directly (CLAUDE_API_KEY, CLAUDE_API_URL and the
"anthropic-dangerous-direct-browser-access" header) which must be removed from
frontend; instead move the secret to a backend/serverless proxy that reads the
key from a server env var and forwards requests to CLAUDE_API_URL, then update
the frontend to call that proxy endpoint (no API key or dangerous header sent
from the browser). In practice: delete CLAUDE_API_KEY usage in
src/services/aiService.ts, implement a server handler that uses the env var to
call Anthropic and returns the response, and change client request code to call
the new proxy endpoint.
---
Nitpick comments:
In `@src/components/AuthButtons.tsx`:
- Around line 183-184: Move the two useState declarations
(isResendingVerification and verificationMessage) to the top of the AuthButtons
component body, before any inner component/constant definitions (e.g., the
GoogleIcon inner component), so all hooks are declared in hook order at the
start of the component; locate the lines where useState is called for
isResendingVerification and verificationMessage and cut/paste them above the
GoogleIcon definition (and any other inner components) to ensure hooks run
consistently.
- Around line 186-200: The two setTimeout calls in handleResendVerification
create timers that can run after unmount; fix by storing their IDs in a ref
(e.g., resendTimerRef) and clearing any existing timer before starting a new
one, use the ref to clearTimeout in a useEffect cleanup to avoid stale-setState,
and update handleResendVerification to set the ref when scheduling the clearing
of setVerificationMessage; keep existing state setters
(setIsResendingVerification, setVerificationMessage) and the resend call
(resendVerificationEmail) intact.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
.env.exampleMOBILE_SUPPORT.mdMOBILE_UX_IMPROVEMENTS.mdSECURITY_TODOS.mdsrc/auth.tssrc/components/AIAssistant.tsxsrc/components/AuthButtons.tsxsrc/components/BottomNav.tsxsrc/components/CircularChart.tsxsrc/components/Dashboard.tsxsrc/components/Sidebar.tsxsrc/components/Timeline.tsxsrc/services/aiService.tssrc/types/schedule.ts
| **Changes:** | ||
| ``` | ||
| - Calculate chart size based on viewport width | ||
| - Add horizontal padding: 16px on mobile, 24px on tablet | ||
| - Center chart using flex container | ||
| - Prevent overflow with: overflow-x: hidden on parent | ||
| - Max width: min(100vw - 32px, 500px) for chart container | ||
| - Position absolutely within centered container | ||
| ``` |
There was a problem hiding this comment.
Add language tags to fenced code blocks (MD040).
Markdownlint reports missing language identifiers. Please add a language (e.g., text, html, css, typescript) to each fenced block for consistency.
🔧 Example fix
-```
+```text
- Calculate chart size based on viewport width
- Add horizontal padding: 16px on mobile, 24px on tablet
- Center chart using flex container
- Prevent overflow with: overflow-x: hidden on parent
- Max width: min(100vw - 32px, 500px) for chart container
- Position absolutely within centered container
-```
+```📝 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.
| **Changes:** | |
| ``` | |
| - Calculate chart size based on viewport width | |
| - Add horizontal padding: 16px on mobile, 24px on tablet | |
| - Center chart using flex container | |
| - Prevent overflow with: overflow-x: hidden on parent | |
| - Max width: min(100vw - 32px, 500px) for chart container | |
| - Position absolutely within centered container | |
| ``` | |
| **Changes:** |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MOBILE_SUPPORT.md` around lines 48 - 56, The fenced code block(s) in
MOBILE_SUPPORT.md are missing language identifiers (MD040); update each
triple-backtick block shown (the block containing the bullet list "Calculate
chart size based on viewport width" etc.) and any other fenced blocks in the
file by adding an appropriate language tag (e.g., ```text, ```css, ```html or
```typescript) after the opening backticks so markdownlint passes and code
blocks render with consistent language metadata.
| ``` | ||
| [ Save Schedule ] [ Delete All ] | ||
| ``` |
There was a problem hiding this comment.
Specify a language for the fenced code block.
This block is missing a language identifier, which triggers MD040.
📝 Suggested fix
-```
+```text
[ Save Schedule ] [ Delete All ]</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 41-41: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MOBILE_UX_IMPROVEMENTS.md` around lines 41 - 43, The fenced code block
containing "[ Save Schedule ] [ Delete All ]" is missing a language identifier
(MD040); update the opening fence from ``` to ```text so the snippet is
explicitly marked as plain text, preserving the content and preventing the MD040
warning.
| ```javascript | ||
| rules_version = '2'; | ||
| service cloud.firestore { | ||
| match /databases/{database}/documents { | ||
| match /schedules/{scheduleId} { | ||
| // Only schedule owner can read/write their own schedules | ||
| allow read, write: if request.auth != null | ||
| && request.auth.uid == resource.data.userId; | ||
|
|
||
| // Only owner can create schedules | ||
| allow create: if request.auth != null | ||
| && request.auth.uid == request.resource.data.userId; | ||
|
|
||
| // Prevent creating more than 10 schedules per user | ||
| allow create: if request.auth != null | ||
| && request.auth.uid == request.resource.data.userId | ||
| && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Firestore security rules multiple allow statements evaluation semantics OR
💡 Result:
In Cloud Firestore Security Rules, multiple allow statements that apply to the same request are combined as a logical OR:
- Within a
match, if anyallowrule for the requested method is satisfied, the request is allowed. [1] - If a document/path matches multiple
matchblocks, the result is still OR across all matchingallowexpressions (“allowed if any condition istrue”). [2]
For “lazy” evaluation (to avoid unnecessary get()/exists() calls):
&&and||are short-circuiting in the Rules language, so in an expression likea || b,bis only evaluated ifais false. [1]
Practical takeaway: if you care about guaranteeing short-circuit order for expensive lookups, put the cheap check first in a single condition using || / && (since that order is defined), rather than relying on the order of separate allow statements. [1]
Sources:
[1] Firebase Docs — Security Rules language (Allow; short-circuiting &&/||)
[2] Firebase Docs — How Security Rules work (Overlapping match statements; OR semantics)
🌐 Web query:
Firestore security rules resource.data vs request.resource.data create update
💡 Result:
In Firestore Security Rules:
resource.data= the current document as stored in Firestore (the “before” state). It exists for reads, updates, deletes; it’s not available oncreatebecause there is no existing document yet. (firebase.google.com)request.resource.data= the document as it would look after the write (the “after” / “pending” state). Forupdate, this is the full merged document (existing fields + changes), not just the fields the client sent. (firebase.google.com)
How to use them by operation
Create
- You validate the incoming document using
request.resource.data(required fields, types, allowed keys, etc.). (firebase.google.com) resource.datais not meaningful on create. (firebase.google.com)
Update
- Use both:
resource.datato check what’s currently stored (e.g., ownership, immutable fields).request.resource.datato check what the document will become.
- To restrict which fields changed, compare them with
diff():request.resource.data.diff(resource.data).affectedKeys()gives the set of fields touched by the update. (firebase.google.com)
Example pattern (owner can update but can’t change author_uid): (firebase.google.com)
allow update: if request.auth.uid == request.resource.data.author_uid
&& request.auth.uid == resource.data.author_uid;
Example field-level update restriction (only allow changing certain fields): (firebase.google.com)
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name','location']);
Citations:
- 1: https://firebase.google.com/docs/rules/basics?utm_source=openai
- 2: https://firebase.google.com/docs/firestore/security/rules-fields?utm_source=openai
- 3: https://firebase.google.com/docs/firestore/security/rules-fields?utm_source=openai
- 4: https://firebase.google.com/docs/rules/basics?utm_source=openai
- 5: https://firebase.google.com/docs/firestore/security/rules-fields?utm_source=openai
- 6: https://firebase.google.com/docs/rules/basics?utm_source=openai
- 7: https://firebase.google.com/docs/firestore/security/rules-fields?utm_source=openai
🌐 Web query:
Firestore rules null checks resource.data on create operation
💡 Result:
On a create in Firestore Security Rules, the document doesn’t exist yet, so resource is null. That means any access to resource.data can throw (and the overall expression evaluates to false/denied), even if it looks like a tautology. (stackoverflow.com)
Use these patterns instead:
1) Check “create vs update” safely
allow write: if resource == null; // create
allow write: if resource != null; // update
(or inside a combined rule)
allow write: if (resource == null && /* create checks */)
|| (resource != null && /* update checks */);
You can also use:
request.method == 'create'
2) Validate fields on create: use request.resource.data
request.resource.data is the incoming (“future”) document for writes; it’s what you should check on create. (firebase.google.com)
Example “null check” on create:
allow create: if request.resource.data.optionalField == null;
3) If you must reference existing data, guard it
allow write: if resource == null
|| resource.data.someField == 'x';
(guard with resource == null before touching resource.data). (stackoverflow.com)
Citations:
- 1: https://stackoverflow.com/questions/66790745/firebase-firestore-security-rule-touching-resource-data-in-create-rule-always-r?utm_source=openai
- 2: https://stackoverflow.com/questions/66790745/firebase-firestore-security-rule-touching-resource-data-in-create-rule-always-r?utm_source=openai
- 3: https://stackoverflow.com/questions/66790745/firebase-firestore-security-rule-touching-resource-data-in-create-rule-always-r?utm_source=openai
- 4: https://firebase.google.com/docs/firestore/security/rules-conditions?utm_source=openai
- 5: https://code.luasoftware.com/tutorials/google-cloud-firestore/firstore-security-rules-prevent-modification-of-certain-fields/?utm_source=openai
- 6: https://stackoverflow.com/questions/66790745/firebase-firestore-security-rule-touching-resource-data-in-create-rule-always-r?utm_source=openai
Consolidate Firestore rules to eliminate bypass of schedule limits and prevent userId changes.
The two allow create rules are evaluated as logical OR; the first allows creation without the scheduleCount check, making the limit bypassable. Additionally, the allow read, write rule uses resource.data.userId which is inaccessible on create (causing the rule to fail), and it lacks protection against updating the userId field. Separate read, create, and update+delete rules with proper field validation are needed.
Consider the proposed rules:
- read: checks
resource.data.userId(safe; resource exists on read) - create: single consolidated rule checking
request.resource.data.userIdand thescheduleCountlimit (prevents OR bypass) - update, delete: enforces both current ownership via
resource.data.userIdand prevents userId reassignment viarequest.resource.data.userId == request.auth.uid
Proposed rules (consolidated)
- allow read, write: if request.auth != null
- && request.auth.uid == resource.data.userId;
-
- allow create: if request.auth != null
- && request.auth.uid == request.resource.data.userId;
-
- allow create: if request.auth != null
- && request.auth.uid == request.resource.data.userId
- && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10;
+ allow read: if request.auth != null
+ && request.auth.uid == resource.data.userId;
+
+ allow create: if request.auth != null
+ && request.auth.uid == request.resource.data.userId
+ && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10;
+
+ allow update, delete: if request.auth != null
+ && request.auth.uid == resource.data.userId
+ && request.resource.data.userId == request.auth.uid;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@SECURITY_TODOS.md` around lines 10 - 27, Consolidate and tighten the
Firestore rules for schedules: remove the duplicate allow create rules and
replace them with a single allow create that validates request.auth != null,
request.resource.data.userId == request.auth.uid, and enforces the scheduleCount
limit via get(...users/$(request.auth.uid)).data.scheduleCount < 10 (use
request.resource.data.* because resource is undefined on create); keep read as
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
and split write into allow update, delete: if request.auth != null &&
resource.data.userId == request.auth.uid && request.resource.data.userId ==
request.auth.uid to prevent changing userId on updates.
| export const signUp = async (email: string, password: string) => { | ||
| const userCredential = await createUserWithEmailAndPassword(auth, email, password); | ||
| // Send email verification immediately after signup | ||
| await sendEmailVerification(userCredential.user); | ||
| return userCredential; | ||
| }; |
There was a problem hiding this comment.
sendEmailVerification failure silently invalidates a successfully created account.
createUserWithEmailAndPassword on line 5 already registers the user in Firebase. If sendEmailVerification on line 7 then throws (e.g., auth/too-many-requests, transient network error), the exception propagates out of signUp, and the caller in AuthButtons.tsx catches it and renders an error message. The user sees a failure, retries, and gets auth/email-already-in-use — their account exists but they can't proceed.
Verification email delivery is a non-critical post-registration step. Wrap it so that its failure doesn't surface as a sign-up failure:
🛡️ Proposed fix
export const signUp = async (email: string, password: string) => {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
- // Send email verification immediately after signup
- await sendEmailVerification(userCredential.user);
+ // Best-effort: send verification email; failure is non-fatal
+ try {
+ await sendEmailVerification(userCredential.user);
+ } catch {
+ // Account is created; user can resend verification later
+ }
return userCredential;
};📝 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.
| export const signUp = async (email: string, password: string) => { | |
| const userCredential = await createUserWithEmailAndPassword(auth, email, password); | |
| // Send email verification immediately after signup | |
| await sendEmailVerification(userCredential.user); | |
| return userCredential; | |
| }; | |
| export const signUp = async (email: string, password: string) => { | |
| const userCredential = await createUserWithEmailAndPassword(auth, email, password); | |
| // Best-effort: send verification email; failure is non-fatal | |
| try { | |
| await sendEmailVerification(userCredential.user); | |
| } catch { | |
| // Account is created; user can resend verification later | |
| } | |
| return userCredential; | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/auth.ts` around lines 4 - 9, The signUp function currently allows
sendEmailVerification failures to bubble up and make the whole signup appear to
fail; change signUp (the async function using createUserWithEmailAndPassword and
sendEmailVerification) to catch errors from sendEmailVerification only (wrap the
sendEmailVerification(userCredential.user) call in a try/catch), log or report
the verification-send error (but do not rethrow), and always return the
successfully created userCredential so callers (e.g., AuthButtons.tsx) see the
signup as succeeded even if the verification email fails to send.
| const handleReloadUser = () => { | ||
| user?.reload().then(() => { | ||
| // Force a re-render by setting user again | ||
| setUser(auth.currentUser); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
handleReloadUser has an unhandled rejection and will not trigger a re-render.
Two problems:
1. Missing .catch(): user?.reload() returns a Promise<void>. Without a .catch(), any rejection (e.g., network failure) becomes an unhandled promise rejection, silently swallowed — the user gets no feedback.
2. Re-render won't fire: Firebase's User.reload() refreshes properties in-place on the same User instance. auth.currentUser after reload returns that same object reference. setUser(auth.currentUser) then calls Object.is(prevUser, nextUser) → true → React bails out without re-rendering. The amber banner will never disappear when the user clicks "Already Verified?", which is the sole purpose of this button.
onAuthStateChanged does not fire on reload(), so the subscription in useEffect won't rescue this either.
Fix: introduce a cheap version counter to force a re-render after reload.
🐛 Proposed fix
+ const [userVersion, setUserVersion] = useState(0);
const handleReloadUser = () => {
- user?.reload().then(() => {
- // Force a re-render by setting user again
- setUser(auth.currentUser);
- });
+ user?.reload().then(() => {
+ setUser(auth.currentUser);
+ setUserVersion(v => v + 1); // force re-render regardless of object identity
+ }).catch(() => {
+ setVerificationMessage('Could not refresh status. Please check your connection.');
+ setTimeout(() => setVerificationMessage(''), 5000);
+ });
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/AuthButtons.tsx` around lines 202 - 207, handleReloadUser
currently calls user?.reload() without a .catch() and then sets the same user
object (auth.currentUser) so React won't re-render; update handleReloadUser to
attach a .catch() that logs or surfaces errors and, on success, increment a
lightweight local state "reloadVersion" (useState number) or toggle a boolean to
force a re-render instead of calling setUser with the same object; keep the
existing setUser(auth.currentUser) if you want but ensure you also bump
reloadVersion so the UI (e.g., amber banner) updates, and reference
user.reload(), auth.currentUser, setUser and the new reloadVersion state in the
fix.
| {currentRoute === 'ai-assistant' && ( | ||
| <AIAssistant | ||
| timeBlocks={timeBlocks} | ||
| messages={aiMessages} | ||
| setMessages={setAiMessages} | ||
| onApplySchedule={(newBlocks) => { | ||
| setTimeBlocks(newBlocks); | ||
| }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect AI assistant schedule generation and validation
rg -n "onApplySchedule|timeBlocks|validateTimeBlock|sortTimeBlocks" -S src/components/AIAssistant.tsx src/utils/timeUtils.tsRepository: omarraf/time-tracker
Length of output: 1252
🏁 Script executed:
#!/bin/bash
# Check handleApplySchedule implementation and surrounding context
sed -n '170,200p' src/components/AIAssistant.tsxRepository: omarraf/time-tracker
Length of output: 905
🏁 Script executed:
#!/bin/bash
# Find and inspect sendMessage function
rg -n "const sendMessage|function sendMessage" src/components/AIAssistant.tsx -A 30Repository: omarraf/time-tracker
Length of output: 46
🏁 Script executed:
#!/bin/bash
# Check imports and sendMessage function in AIAssistant.tsx
head -50 src/components/AIAssistant.tsx | grep -n "import\|sendMessage"
# Also search for sendMessage anywhere in the file
rg -n "sendMessage" src/components/AIAssistant.tsx -B 2 -A 5Repository: omarraf/time-tracker
Length of output: 1035
🏁 Script executed:
#!/bin/bash
# Check aiService implementation
cat src/services/aiService.tsRepository: omarraf/time-tracker
Length of output: 4975
🏁 Script executed:
#!/bin/bash
# Find manual block creation/validation in Dashboard.tsx
rg -n "addTimeBlock|validateTimeBlock|setTimeBlocks" src/components/Dashboard.tsx -B 3 -A 8 | head -80Repository: omarraf/time-tracker
Length of output: 2876
🏁 Script executed:
#!/bin/bash
# Find onApplySchedule handler in Dashboard.tsx
rg -n "onApplySchedule" src/components/Dashboard.tsx -B 2 -A 5Repository: omarraf/time-tracker
Length of output: 295
Add validation and sorting to AI-generated blocks before applying to state.
onApplySchedule applies blocks directly without validation, unlike manual block creation which calls validateTimeBlock() and sortTimeBlocks(). If the AI returns overlapping or unsorted blocks, they bypass the validation enforced for manual blocks, potentially breaking downstream rendering and state expectations.
🛡️ Suggested in-place validation
- onApplySchedule={(newBlocks) => {
- setTimeBlocks(newBlocks);
- }}
+ onApplySchedule={(newBlocks) => {
+ const sorted = sortTimeBlocks(newBlocks);
+ const validated: TimeBlock[] = [];
+
+ for (const block of sorted) {
+ const { valid, message } = validateTimeBlock(
+ { startTime: block.startTime, endTime: block.endTime },
+ validated
+ );
+ if (!valid) {
+ alert(message);
+ return;
+ }
+ validated.push(block);
+ }
+
+ setTimeBlocks(validated);
+ }}📝 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.
| {currentRoute === 'ai-assistant' && ( | |
| <AIAssistant | |
| timeBlocks={timeBlocks} | |
| messages={aiMessages} | |
| setMessages={setAiMessages} | |
| onApplySchedule={(newBlocks) => { | |
| setTimeBlocks(newBlocks); | |
| }} | |
| /> | |
| {currentRoute === 'ai-assistant' && ( | |
| <AIAssistant | |
| timeBlocks={timeBlocks} | |
| messages={aiMessages} | |
| setMessages={setAiMessages} | |
| onApplySchedule={(newBlocks) => { | |
| const sorted = sortTimeBlocks(newBlocks); | |
| const validated: TimeBlock[] = []; | |
| for (const block of sorted) { | |
| const { valid, message } = validateTimeBlock( | |
| { startTime: block.startTime, endTime: block.endTime }, | |
| validated | |
| ); | |
| if (!valid) { | |
| alert(message); | |
| return; | |
| } | |
| validated.push(block); | |
| } | |
| setTimeBlocks(validated); | |
| }} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/Dashboard.tsx` around lines 641 - 649, The AI-assistant
callback currently applies blocks directly; instead, in the onApplySchedule
passed to AIAssistant validate each incoming block with validateTimeBlock, drop
or collect invalid ones, then pass the filtered list through sortTimeBlocks
before calling setTimeBlocks(newBlocksSorted); reference the AIAssistant prop
onApplySchedule, the validateTimeBlock function, sortTimeBlocks helper, and the
setTimeBlocks state setter when making this change so AI-generated blocks follow
the same validation/sorting rules as manual blocks.
| // Calculate pixel height for smart text fitting | ||
| const pixelHeight = (segment.duration / totalDayMinutes) * timelineHeight; | ||
| const isVeryShort = pixelHeight < 28; | ||
| const isShort = pixelHeight < 44; | ||
| const isMedium = pixelHeight < 60; | ||
|
|
||
| return ( | ||
| <div | ||
| key={`${block.id}-${index}`} | ||
| className="absolute left-12 sm:left-14 right-2 sm:right-4 rounded-lg cursor-pointer transition-all hover:shadow-lg active:scale-95 sm:hover:scale-[1.02] group touch-manipulation" | ||
| className="absolute left-12 sm:left-14 right-2 sm:right-4 rounded-lg cursor-pointer transition-all hover:shadow-lg active:scale-95 sm:hover:scale-[1.02] group touch-manipulation overflow-hidden" | ||
| style={{ | ||
| top: `${top}%`, | ||
| height: `${height}%`, | ||
| backgroundColor: block.color, | ||
| minHeight: '30px', | ||
| minHeight: '20px', | ||
| }} |
There was a problem hiding this comment.
Touch targets may be too small on mobile; align minHeight with label fitting.
Reducing minHeight to 20px can make short blocks difficult to tap on touch devices and conflicts with the 44px touch target guidance in MOBILE_SUPPORT.md. Also, label fitting uses pixelHeight derived from duration, so if minHeight inflates the rendered size, the UI still chooses overly-compact layouts.
💡 Suggested adjustment
- const pixelHeight = (segment.duration / totalDayMinutes) * timelineHeight;
- const isVeryShort = pixelHeight < 28;
+ const rawPixelHeight = (segment.duration / totalDayMinutes) * timelineHeight;
+ const minPixelHeight = isMobile ? 44 : 20;
+ const pixelHeight = Math.max(rawPixelHeight, minPixelHeight);
+ const isVeryShort = pixelHeight < 28;
...
- minHeight: '20px',
+ minHeight: `${minPixelHeight}px`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/Timeline.tsx` around lines 225 - 240, The minHeight of 20px
makes touch targets too small and the text-fitting booleans (pixelHeight,
isVeryShort, isShort, isMedium) still use raw duration-based pixelHeight, so
short blocks get compact layouts even when minHeight inflates their visual size;
change the block render to use a 44px minimum touch target (style minHeight:
'44px') and adjust the sizing logic: compute a renderedPixelHeight =
Math.max(pixelHeight, 44) (or convert minHeight to the same coordinate system)
and base isVeryShort/isShort/isMedium on renderedPixelHeight instead of
pixelHeight so label/layout decisions reflect the actual rendered height on
touch devices while keeping the minHeight enforced on the div.
src/services/aiService.ts
Outdated
| export const parseScheduleFromResponse = (text: string): TimeBlock[] | null => { | ||
| const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/); | ||
| if (!scheduleMatch) return null; | ||
|
|
||
| try { | ||
| const parsed = JSON.parse(scheduleMatch[1]); | ||
| if (!Array.isArray(parsed)) return null; | ||
|
|
||
| return parsed.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({ | ||
| id: crypto.randomUUID(), | ||
| startTime: block.startTime, | ||
| endTime: block.endTime, | ||
| label: block.label, | ||
| color: block.color || '#60a5fa', | ||
| order: index, | ||
| })); |
There was a problem hiding this comment.
Validate AI-produced time blocks before creating IDs.
parseScheduleFromResponse trusts AI output; invalid time formats, non‑5‑minute increments, or overlaps can propagate corrupted data into the scheduler. Add input validation and discard (or normalize) invalid blocks.
🛡️ Suggested validation pass
export const parseScheduleFromResponse = (text: string): TimeBlock[] | null => {
const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/);
if (!scheduleMatch) return null;
try {
const parsed = JSON.parse(scheduleMatch[1]);
if (!Array.isArray(parsed)) return null;
+ const isValidTime = (t: string) => {
+ const m = /^(\d{2}):(\d{2})$/.exec(t);
+ if (!m) return false;
+ const hh = Number(m[1]);
+ const mm = Number(m[2]);
+ return hh >= 0 && hh <= 23 && mm >= 0 && mm < 60 && mm % 5 === 0;
+ };
+ const sanitized = parsed.filter((b: any) =>
+ b && isValidTime(b.startTime) && isValidTime(b.endTime)
+ );
+ if (sanitized.length === 0) return null;
- return parsed.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({
+ return sanitized.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({
id: crypto.randomUUID(),
startTime: block.startTime,
endTime: block.endTime,
label: block.label,
color: block.color || '#60a5fa',
order: index,
}));
} catch {
return null;
}
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/aiService.ts` around lines 45 - 60, parseScheduleFromResponse
currently trusts the AI JSON and creates TimeBlock IDs immediately; add a
validation/sanitization pass before mapping to TimeBlock: parse the JSON as now,
then filter into a sanitized array (e.g., sanitized) that only keeps objects
with required properties (startTime, endTime, label) where startTime/endTime
match /^(\d{2}):(\d{2})$/ and hours 0–23, minutes 0–59 and minutes % 5 === 0,
and optionally ensure startTime < endTime and no overlapping blocks; if
sanitized is empty return null, then map sanitized to TimeBlock objects (id via
crypto.randomUUID(), order = index, default color '#60a5fa'), leaving
parseScheduleFromResponse and TimeBlock shape intact.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/components/Dashboard.tsx (1)
655-663:⚠️ Potential issue | 🟠 MajorAdd validation and sorting to AI-generated blocks before applying to state.
onApplyScheduleapplies blocks directly without the validation that manual block creation uses (validateTimeBlock,sortTimeBlocks). AI-generated blocks could contain overlaps or be unsorted, bypassing safeguards enforced elsewhere.🛡️ Proposed validation
onApplySchedule={(newBlocks) => { - setTimeBlocks(newBlocks); + const sorted = sortTimeBlocks(newBlocks); + const validated: TimeBlock[] = []; + for (const block of sorted) { + const { valid } = validateTimeBlock( + { startTime: block.startTime, endTime: block.endTime }, + validated + ); + if (valid) { + validated.push(block); + } + } + setTimeBlocks(validated); }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Dashboard.tsx` around lines 655 - 663, onApplySchedule currently sets AI-generated blocks directly (in the AIAssistant prop) which bypasses manual-creation safeguards; instead validate each incoming block using validateTimeBlock, discard or report any invalid blocks, then sort the resulting list with sortTimeBlocks before calling setTimeBlocks (e.g., in the onApplySchedule callback: run newBlocks.filter(validateTimeBlock) and then sortTimeBlocks(...) and pass that to setTimeBlocks), ensuring you reference the AIAssistant prop onApplySchedule, the validateTimeBlock function, sortTimeBlocks function, and setTimeBlocks state updater.src/services/aiService.ts (1)
65-77:⚠️ Potential issue | 🟠 MajorGuard API response shape before mapping
timeBlocks.At Line 68,
data.timeBlocks?.map(...)still throws iftimeBlocksis a truthy non-array value. Validate payload shape first, then map.Proposed fix
- const data = await response.json(); + const data = await response.json(); - const enrichedBlocks = data.timeBlocks?.map( + const validBlocks = Array.isArray(data?.timeBlocks) + ? data.timeBlocks.filter( + (block: unknown): block is { startTime: string; endTime: string; label: string; color?: string } => + !!block && + typeof block === 'object' && + typeof (block as any).startTime === 'string' && + typeof (block as any).endTime === 'string' && + typeof (block as any).label === 'string' + ) + : undefined; + + const enrichedBlocks = validBlocks?.map( (block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({ id: crypto.randomUUID(), startTime: block.startTime, endTime: block.endTime, label: block.label, color: block.color || '#60a5fa', order: index, }) );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/aiService.ts` around lines 65 - 77, The code assumes data.timeBlocks is an array when creating enrichedBlocks which will throw if timeBlocks is a truthy non-array; change the mapping to first validate the shape with Array.isArray(data.timeBlocks) (or a type guard) and only map when true, otherwise set enrichedBlocks to an empty array or a sensible default so the logic that uses enrichedBlocks won't crash; refer to the variables/enriched mapping logic (data, timeBlocks, enrichedBlocks, crypto.randomUUID) and replace the direct data.timeBlocks?.map(...) with a guarded expression like Array.isArray(data.timeBlocks) ? data.timeBlocks.map(...) : [].functions/src/index.ts (1)
68-83:⚠️ Potential issue | 🟠 MajorSanitize parsed schedule blocks before returning them.
At Line 76, parsed AI output is trusted without enforcing the file’s own scheduling rules (format, 5-minute increments, non-overlap). Invalid blocks can propagate downstream.
Proposed fix
function parseScheduleFromResponse(text: string): TimeBlock[] | null { const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/); if (!scheduleMatch) return null; try { const parsed = JSON.parse(scheduleMatch[1]); if (!Array.isArray(parsed)) return null; + const isValidTime = (t: string) => { + const m = /^(\d{2}):(\d{2})$/.exec(t); + if (!m) return false; + const hh = Number(m[1]); + const mm = Number(m[2]); + return hh >= 0 && hh <= 23 && mm >= 0 && mm <= 59 && mm % 5 === 0; + }; + const toMinutes = (t: string) => { + const [h, m] = t.split(":").map(Number); + return h * 60 + m; + }; + + const sanitized = parsed + .filter((b: any) => + b && + typeof b.startTime === "string" && + typeof b.endTime === "string" && + typeof b.label === "string" && + isValidTime(b.startTime) && + isValidTime(b.endTime) && + toMinutes(b.startTime) < toMinutes(b.endTime) + ) + .sort((a: any, b: any) => toMinutes(a.startTime) - toMinutes(b.startTime)); + + for (let i = 1; i < sanitized.length; i++) { + if (toMinutes(sanitized[i - 1].endTime) > toMinutes(sanitized[i].startTime)) { + return null; + } + } + if (sanitized.length === 0) return null; - return parsed.map( + return sanitized.map( (block: { startTime: string; endTime: string; label: string; color: string }) => ({ startTime: block.startTime, endTime: block.endTime, label: block.label, color: block.color || "#60a5fa", }) ); } catch { return null; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@functions/src/index.ts` around lines 68 - 83, The parseScheduleFromResponse function currently trusts the AI JSON; update it to sanitize and validate blocks before returning: after parsing the array in parseScheduleFromResponse, add helpers (e.g., isValidTime and toMinutes) to ensure startTime/endTime are "HH:MM", minutes are multiples of 5, times are in-range, start < end, filter out invalid entries, sort by start time, detect overlapping adjacent blocks (return null if any overlap) and return null if the sanitized list is empty; then map over the sanitized array (instead of the raw parsed array) to produce the TimeBlock objects (keep fallback color logic) and preserve existing try/catch behavior.
🧹 Nitpick comments (5)
.gitignore (1)
30-31: Consider if the specific pattern is needed.The
server/distpattern may be redundant since the generaldistpattern on line 11 already matches any directory nameddistanywhere in the repository, includingserver/dist. However, keeping it can serve as documentation to clarify which build artifacts are being ignored.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore around lines 30 - 31, The line "server/dist" is likely redundant because the existing "dist" pattern already ignores any directory named dist (including server/dist); either remove the "server/dist" entry to avoid duplication or keep it with an inline comment clarifying it's intentional documentation for the server build artifact. Locate the .gitignore entries for "dist" and "server/dist" and remove the latter or add a clarifying comment next to "server/dist" indicating it's kept for documentation.server/src/index.ts (2)
161-164: Add defensive check for emptyresponse.contentarray.Accessing
response.content[0]assumes the array is non-empty. While unlikely with standard Anthropic responses, a defensive check prevents potential runtime errors.🛡️ Proposed defensive check
- const assistantText = - response.content[0].type === 'text' - ? response.content[0].text - : 'Sorry, I could not generate a response.'; + const firstContent = response.content[0]; + const assistantText = + firstContent?.type === 'text' + ? firstContent.text + : 'Sorry, I could not generate a response.';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 161 - 164, The current assignment to assistantText reads response.content[0] without ensuring response.content exists or has elements; add a defensive check before accessing index 0 (e.g., verify response.content && response.content.length > 0) and only then evaluate response.content[0].type === 'text', otherwise set assistantText to the fallback message (or empty string). Update the logic where assistantText is computed (the assignment using response.content[0]) to guard against undefined content and preserve the existing fallback behavior.
88-107: Consider validating parsed schedule blocks for required fields and time format.
parseScheduleFromResponseparses JSON but doesn't validate thatstartTime/endTimeare valid "HH:MM" strings or that required fields exist. Malformed AI output could propagate invalid data to the frontend.♻️ Proposed validation
+const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/; + +function isValidTimeBlock(block: unknown): block is { startTime: string; endTime: string; label: string; color?: string } { + if (typeof block !== 'object' || block === null) return false; + const b = block as Record<string, unknown>; + return ( + typeof b.startTime === 'string' && TIME_REGEX.test(b.startTime) && + typeof b.endTime === 'string' && TIME_REGEX.test(b.endTime) && + typeof b.label === 'string' + ); +} + function parseScheduleFromResponse(text: string): TimeBlock[] | null { const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/); if (!scheduleMatch) return null; try { const parsed = JSON.parse(scheduleMatch[1]); if (!Array.isArray(parsed)) return null; - return parsed.map( - (block: { startTime: string; endTime: string; label: string; color: string }) => ({ + const validBlocks = parsed.filter(isValidTimeBlock); + if (validBlocks.length === 0) return null; + + return validBlocks.map((block) => ({ startTime: block.startTime, endTime: block.endTime, label: block.label, color: block.color || '#60a5fa', - }) - ); + })); } catch { return null; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 88 - 107, In parseScheduleFromResponse, validate each parsed block before returning to avoid propagating malformed AI output: after JSON.parse and confirming an array, filter entries using a helper like isValidTimeBlock that ensures required fields (startTime, endTime, label) exist and startTime/endTime match an HH:MM 24‑hour regex and that startTime < endTime if applicable; if no valid blocks remain return null, otherwise map the filtered blocks to the TimeBlock shape and apply the default color ('#60a5fa') when color is missing.src/components/ScheduleListPanel.tsx (1)
16-21: Consider adding accessibility attributes to the modal.The modal lacks
role,aria-modal, andaria-labelledbyattributes. For better screen reader support, consider adding these attributes and implementing focus trapping.♿ Proposed accessibility improvements
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm" onClick={onClose}> + <div + className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm" + onClick={onClose} + role="dialog" + aria-modal="true" + aria-labelledby="schedule-panel-title" + > <div className="bg-white rounded-2xl shadow-2xl border border-gray-200 w-full max-w-lg max-h-[80vh] flex flex-col" onClick={(e) => e.stopPropagation()} > {/* Header */} <div className="flex items-center justify-between px-5 py-4 border-b border-gray-100"> <div> - <h3 className="text-base font-semibold text-gray-900">Schedule Overview</h3> + <h3 id="schedule-panel-title" className="text-base font-semibold text-gray-900">Schedule Overview</h3>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ScheduleListPanel.tsx` around lines 16 - 21, The modal in ScheduleListPanel is missing accessibility attributes and focus management; update the outer container (the element with onClose) to include role="dialog" and aria-modal="true", add aria-labelledby pointing to the modal title element (give the title a stable id), and ensure the inner container (the element that calls e.stopPropagation()) remains focusable; implement focus trapping and initial focus via a React ref/useEffect inside ScheduleListPanel (also handle Escape to call onClose) so screen readers and keyboard users can interact correctly.src/services/aiService.ts (1)
43-53: Consider adding an explicit request timeout for the AI call.At Line 43, a stalled network call can hang the UI path for too long. Using
AbortControllerhere would improve resilience.Proposed refactor
- const response = await fetch(`${API_BASE_URL}/api/ai/message`, { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + const response = await fetch(`${API_BASE_URL}/api/ai/message`, { method: 'POST', + signal: controller.signal, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ messages, timeBlocks: currentTimeBlocks, }), }); + clearTimeout(timeout);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/aiService.ts` around lines 43 - 53, Add an explicit request timeout using AbortController around the fetch to /api/ai/message: create an AbortController, pass controller.signal into the fetch options, start a setTimeout that calls controller.abort() after a sensible timeout (e.g., 10s), and clear the timer once fetch resolves; update the fetch error handling in the same function (the fetch block that references API_BASE_URL, token, messages, currentTimeBlocks) to treat aborts appropriately (propagate or map to a timeout error) so the UI path won't hang on stalled network calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@functions/src/index.ts`:
- Around line 133-136: The code reads response.content[0] without checking the
array is non-empty, which can throw; update the assistantText assignment to
first safely obtain the first item (e.g., const first = response.content &&
response.content.length ? response.content[0] : undefined or use optional
chaining) and then check first?.type === "text" and first?.text for the value,
falling back to the existing default "Sorry, I could not generate a response.";
this change touches the assistantText assignment and the response.content
access.
- Around line 108-119: Validate the incoming request.data fully before using it:
ensure request.data conforms to SendAIMessageData by checking messages is an
array and each element has a valid role ("user" or "assistant") and non-empty
content, and ensure timeBlocks (if provided) is an array with the expected shape
before calling formatScheduleContext; if any check fails, throw an
HttpsError("invalid-argument", ...) with a clear message. Update the validation
around messages/timeBlocks where apiMessages is built and before calling
formatScheduleContext (referencing messages, timeBlocks, SendAIMessageData,
formatScheduleContext, and apiMessages) to prevent malformed items from reaching
the map and causing runtime errors.
In `@server/src/index.ts`:
- Around line 31-37: The server TimeBlock interface is missing the frontend
fields id and order; update the TimeBlock interface in server/src/index.ts to
include id: string and order: number, and then update any code that constructs,
validates, parses, or returns TimeBlock objects to preserve and use these fields
so incoming requests and responses match the frontend type (check places that
create/serialize TimeBlock instances after parsing or DB operations to ensure id
and order are maintained).
- Around line 15-22: The CORS middleware currently limits allowed methods to
['POST'] which blocks browser requests to the GET /health endpoint; update the
CORS configuration used in app.use(cors(...)) to include 'GET' (or include both
'GET' and 'POST') so the GET /health handler can be called from allowed origins
without preflight failures, ensuring the methods array passed to cors includes
'GET' when registering the middleware.
---
Duplicate comments:
In `@functions/src/index.ts`:
- Around line 68-83: The parseScheduleFromResponse function currently trusts the
AI JSON; update it to sanitize and validate blocks before returning: after
parsing the array in parseScheduleFromResponse, add helpers (e.g., isValidTime
and toMinutes) to ensure startTime/endTime are "HH:MM", minutes are multiples of
5, times are in-range, start < end, filter out invalid entries, sort by start
time, detect overlapping adjacent blocks (return null if any overlap) and return
null if the sanitized list is empty; then map over the sanitized array (instead
of the raw parsed array) to produce the TimeBlock objects (keep fallback color
logic) and preserve existing try/catch behavior.
In `@src/components/Dashboard.tsx`:
- Around line 655-663: onApplySchedule currently sets AI-generated blocks
directly (in the AIAssistant prop) which bypasses manual-creation safeguards;
instead validate each incoming block using validateTimeBlock, discard or report
any invalid blocks, then sort the resulting list with sortTimeBlocks before
calling setTimeBlocks (e.g., in the onApplySchedule callback: run
newBlocks.filter(validateTimeBlock) and then sortTimeBlocks(...) and pass that
to setTimeBlocks), ensuring you reference the AIAssistant prop onApplySchedule,
the validateTimeBlock function, sortTimeBlocks function, and setTimeBlocks state
updater.
In `@src/services/aiService.ts`:
- Around line 65-77: The code assumes data.timeBlocks is an array when creating
enrichedBlocks which will throw if timeBlocks is a truthy non-array; change the
mapping to first validate the shape with Array.isArray(data.timeBlocks) (or a
type guard) and only map when true, otherwise set enrichedBlocks to an empty
array or a sensible default so the logic that uses enrichedBlocks won't crash;
refer to the variables/enriched mapping logic (data, timeBlocks, enrichedBlocks,
crypto.randomUUID) and replace the direct data.timeBlocks?.map(...) with a
guarded expression like Array.isArray(data.timeBlocks) ?
data.timeBlocks.map(...) : [].
---
Nitpick comments:
In @.gitignore:
- Around line 30-31: The line "server/dist" is likely redundant because the
existing "dist" pattern already ignores any directory named dist (including
server/dist); either remove the "server/dist" entry to avoid duplication or keep
it with an inline comment clarifying it's intentional documentation for the
server build artifact. Locate the .gitignore entries for "dist" and
"server/dist" and remove the latter or add a clarifying comment next to
"server/dist" indicating it's kept for documentation.
In `@server/src/index.ts`:
- Around line 161-164: The current assignment to assistantText reads
response.content[0] without ensuring response.content exists or has elements;
add a defensive check before accessing index 0 (e.g., verify response.content &&
response.content.length > 0) and only then evaluate response.content[0].type ===
'text', otherwise set assistantText to the fallback message (or empty string).
Update the logic where assistantText is computed (the assignment using
response.content[0]) to guard against undefined content and preserve the
existing fallback behavior.
- Around line 88-107: In parseScheduleFromResponse, validate each parsed block
before returning to avoid propagating malformed AI output: after JSON.parse and
confirming an array, filter entries using a helper like isValidTimeBlock that
ensures required fields (startTime, endTime, label) exist and startTime/endTime
match an HH:MM 24‑hour regex and that startTime < endTime if applicable; if no
valid blocks remain return null, otherwise map the filtered blocks to the
TimeBlock shape and apply the default color ('#60a5fa') when color is missing.
In `@src/components/ScheduleListPanel.tsx`:
- Around line 16-21: The modal in ScheduleListPanel is missing accessibility
attributes and focus management; update the outer container (the element with
onClose) to include role="dialog" and aria-modal="true", add aria-labelledby
pointing to the modal title element (give the title a stable id), and ensure the
inner container (the element that calls e.stopPropagation()) remains focusable;
implement focus trapping and initial focus via a React ref/useEffect inside
ScheduleListPanel (also handle Escape to call onClose) so screen readers and
keyboard users can interact correctly.
In `@src/services/aiService.ts`:
- Around line 43-53: Add an explicit request timeout using AbortController
around the fetch to /api/ai/message: create an AbortController, pass
controller.signal into the fetch options, start a setTimeout that calls
controller.abort() after a sensible timeout (e.g., 10s), and clear the timer
once fetch resolves; update the fetch error handling in the same function (the
fetch block that references API_BASE_URL, token, messages, currentTimeBlocks) to
treat aborts appropriately (propagate or map to a timeout error) so the UI path
won't hang on stalled network calls.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
functions/package-lock.jsonis excluded by!**/package-lock.jsonserver/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (13)
.env.example.gitignorefirebase.jsonfunctions/package.jsonfunctions/src/index.tsfunctions/tsconfig.jsonserver/package.jsonserver/src/index.tsserver/tsconfig.jsonsrc/components/Dashboard.tsxsrc/components/ScheduleListPanel.tsxsrc/firebase.tssrc/services/aiService.ts
✅ Files skipped from review due to trivial changes (1)
- server/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
- .env.example
| const { messages, timeBlocks } = request.data as SendAIMessageData; | ||
|
|
||
| if (!messages || !Array.isArray(messages)) { | ||
| throw new HttpsError("invalid-argument", "Messages array is required."); | ||
| } | ||
|
|
||
| const scheduleContext = formatScheduleContext(timeBlocks || []); | ||
|
|
||
| const apiMessages = messages.map((msg) => ({ | ||
| role: msg.role as "user" | "assistant", | ||
| content: msg.content, | ||
| })); |
There was a problem hiding this comment.
Validate request.data schema before processing.
At Line 108, only messages array existence is checked. Invalid timeBlocks and malformed message items can still flow into Line 114/Line 116 and fail at runtime.
Proposed fix
- const { messages, timeBlocks } = request.data as SendAIMessageData;
+ const { messages, timeBlocks } = (request.data ?? {}) as Partial<SendAIMessageData>;
- if (!messages || !Array.isArray(messages)) {
+ const isValidMessage = (m: unknown): m is ChatMessage =>
+ !!m &&
+ typeof m === "object" &&
+ ((m as ChatMessage).role === "user" || (m as ChatMessage).role === "assistant") &&
+ typeof (m as ChatMessage).content === "string";
+
+ if (!Array.isArray(messages) || !messages.every(isValidMessage)) {
throw new HttpsError("invalid-argument", "Messages array is required.");
}
+
+ if (timeBlocks != null && !Array.isArray(timeBlocks)) {
+ throw new HttpsError("invalid-argument", "timeBlocks must be an array.");
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@functions/src/index.ts` around lines 108 - 119, Validate the incoming
request.data fully before using it: ensure request.data conforms to
SendAIMessageData by checking messages is an array and each element has a valid
role ("user" or "assistant") and non-empty content, and ensure timeBlocks (if
provided) is an array with the expected shape before calling
formatScheduleContext; if any check fails, throw an
HttpsError("invalid-argument", ...) with a clear message. Update the validation
around messages/timeBlocks where apiMessages is built and before calling
formatScheduleContext (referencing messages, timeBlocks, SendAIMessageData,
formatScheduleContext, and apiMessages) to prevent malformed items from reaching
the map and causing runtime errors.
| const assistantText = | ||
| response.content[0].type === "text" | ||
| ? response.content[0].text | ||
| : "Sorry, I could not generate a response."; |
There was a problem hiding this comment.
Avoid direct indexing into response.content without a guard.
At Line 134, response.content[0] can throw if the content array is empty. Add a safe lookup before reading .type.
Proposed fix
- const assistantText =
- response.content[0].type === "text"
- ? response.content[0].text
- : "Sorry, I could not generate a response.";
+ const firstPart = Array.isArray(response.content) ? response.content[0] : undefined;
+ const assistantText =
+ firstPart && firstPart.type === "text"
+ ? firstPart.text
+ : "Sorry, I could not generate a response.";📝 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.
| const assistantText = | |
| response.content[0].type === "text" | |
| ? response.content[0].text | |
| : "Sorry, I could not generate a response."; | |
| const firstPart = Array.isArray(response.content) ? response.content[0] : undefined; | |
| const assistantText = | |
| firstPart && firstPart.type === "text" | |
| ? firstPart.text | |
| : "Sorry, I could not generate a response."; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@functions/src/index.ts` around lines 133 - 136, The code reads
response.content[0] without checking the array is non-empty, which can throw;
update the assistantText assignment to first safely obtain the first item (e.g.,
const first = response.content && response.content.length ? response.content[0]
: undefined or use optional chaining) and then check first?.type === "text" and
first?.text for the value, falling back to the existing default "Sorry, I could
not generate a response."; this change touches the assistantText assignment and
the response.content access.
| // Types | ||
| interface TimeBlock { | ||
| startTime: string; | ||
| endTime: string; | ||
| label: string; | ||
| color: string; | ||
| } |
There was a problem hiding this comment.
Server TimeBlock type is missing id and order fields.
The frontend TimeBlock type in src/types/schedule.ts includes id: string and order: number fields. The server type lacks these, which could cause type mismatches when the frontend sends blocks to the server or when parsed blocks are returned without these fields.
🛠️ Proposed fix to align with frontend type
interface TimeBlock {
+ id?: string;
startTime: string;
endTime: string;
label: string;
color: string;
+ order?: number;
}📝 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.
| // Types | |
| interface TimeBlock { | |
| startTime: string; | |
| endTime: string; | |
| label: string; | |
| color: string; | |
| } | |
| interface TimeBlock { | |
| id?: string; | |
| startTime: string; | |
| endTime: string; | |
| label: string; | |
| color: string; | |
| order?: number; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/src/index.ts` around lines 31 - 37, The server TimeBlock interface is
missing the frontend fields id and order; update the TimeBlock interface in
server/src/index.ts to include id: string and order: number, and then update any
code that constructs, validates, parses, or returns TimeBlock objects to
preserve and use these fields so incoming requests and responses match the
frontend type (check places that create/serialize TimeBlock instances after
parsing or DB operations to ensure id and order are maintained).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
server/src/index.ts (4)
10-12: Firebase Admin initialization may fail without credentials in non-GCP environments.When running outside Google Cloud (e.g., local development), Firebase Admin SDK requires explicit credentials. Consider documenting the required environment setup or adding a check.
📝 Suggested improvement for local development support
// Initialize Firebase Admin for auth token verification +// Note: In GCP environments, credentials are auto-discovered. +// For local development, set GOOGLE_APPLICATION_CREDENTIALS env var. admin.initializeApp({ projectId: process.env.FIREBASE_PROJECT_ID, });Or conditionally load credentials:
admin.initializeApp({ projectId: process.env.FIREBASE_PROJECT_ID, credential: process.env.GOOGLE_APPLICATION_CREDENTIALS ? admin.credential.applicationDefault() : undefined, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 10 - 12, The Firebase Admin initialization call (admin.initializeApp) can fail in non-GCP/local environments because credentials may be missing; update the initialization to conditionally provide credentials by detecting GOOGLE_APPLICATION_CREDENTIALS or similar env var and pass admin.credential.applicationDefault() (or a parsed service account) when present, otherwise omit credential to allow GCP default behavior, and add a clear error log or throw if neither projectId nor credentials are available so startup fails fast with guidance for local setup.
176-185: Error handling relies on string matching which is fragile.Checking
error.message.includes('authorization')may not catch all auth-related errors (e.g., token expired, invalid signature). Consider throwing a custom error type fromverifyAuthfor more reliable error classification.💡 Alternative approach with custom error
class AuthError extends Error { constructor(message: string) { super(message); this.name = 'AuthError'; } } // In verifyAuth: throw new AuthError('Missing or invalid authorization header'); // In catch block: if (error instanceof AuthError) { res.status(401).json({ error: 'Unauthorized. Please sign in.' }); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 176 - 185, Replace fragile string matching with a custom error type: add an AuthError class and have verifyAuth throw AuthError for any auth failures (missing header, token expired, invalid signature, etc.), then in the catch block in server/src/index.ts replace the message.includes('authorization') check with an instanceof AuthError check to return 401 and otherwise fall through to 500; update any call sites of verifyAuth to ensure they propagate AuthError as thrown.
89-108: Consider validating parsed time block fields before use.The function assumes the AI response contains properly structured objects with string fields. Malformed AI responses could lead to runtime issues when these values are used downstream (e.g.,
undefined.split(':')in time sorting).🛡️ Add basic field validation
return parsed.map( - (block: { startTime: string; endTime: string; label: string; color: string }) => ({ - startTime: block.startTime, - endTime: block.endTime, - label: block.label, - color: block.color || '#60a5fa', - }) - ); + (block: unknown) => { + if ( + typeof block !== 'object' || + block === null || + typeof (block as Record<string, unknown>).startTime !== 'string' || + typeof (block as Record<string, unknown>).endTime !== 'string' || + typeof (block as Record<string, unknown>).label !== 'string' + ) { + return null; + } + const b = block as { startTime: string; endTime: string; label: string; color?: string }; + return { + startTime: b.startTime, + endTime: b.endTime, + label: b.label, + color: typeof b.color === 'string' ? b.color : '#60a5fa', + }; + } + ).filter((b): b is TimeBlock => b !== null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 89 - 108, parseScheduleFromResponse assumes each parsed block has valid string fields which can cause downstream runtime errors; validate each object from JSON.parse inside parseScheduleFromResponse by checking that startTime and endTime are non-empty strings matching a time pattern (e.g., HH:MM), label is a string, and color is a string (fallback to '#60a5fa' if missing), filter out any blocks that fail validation, and return null or an empty array if no valid blocks remain; reference the parseScheduleFromResponse function and the mapped block shape (startTime, endTime, label, color) when making these checks.
150-160: Consider adding a timeout for the Anthropic API call.External API calls without timeouts can cause requests to hang indefinitely, potentially exhausting server resources under load. The Anthropic SDK supports a
timeoutoption.⏱️ Add timeout configuration
- const client = new Anthropic({ apiKey: claudeApiKey }); + const client = new Anthropic({ + apiKey: claudeApiKey, + timeout: 60000, // 60 second timeout + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 150 - 160, The Anthropic API call (client.messages.create) currently has no timeout and can hang; add a timeout option to the call (or when constructing the Anthropic client) so requests fail fast—e.g., use a configurable ANTHROPIC_TIMEOUT_MS env var with a sensible default (e.g., 30000 ms) and pass that value into the timeout option when calling client.messages.create (or into new Anthropic({...})), ensuring the timeout applies to the request that assigns to response and uses claudeApiKey/model/SYSTEM_PROMPT/scheduleContext.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/src/index.ts`:
- Around line 162-165: The current assignment to assistantText directly indexes
response.content[0] (seen in the assistantText declaration) which can throw or
behave incorrectly if response.content is empty or undefined; change the logic
to first verify response.content exists and has at least one item and that that
item's type and text are defined (e.g., check response.content?.length > 0 and
response.content[0].type === 'text' and response.content[0].text) before using
it, otherwise use the fallback string—update the assistantText computation to
perform these defensive checks (or use optional chaining and a safe default) so
accessing response.content[0] cannot cause a runtime error.
---
Nitpick comments:
In `@server/src/index.ts`:
- Around line 10-12: The Firebase Admin initialization call
(admin.initializeApp) can fail in non-GCP/local environments because credentials
may be missing; update the initialization to conditionally provide credentials
by detecting GOOGLE_APPLICATION_CREDENTIALS or similar env var and pass
admin.credential.applicationDefault() (or a parsed service account) when
present, otherwise omit credential to allow GCP default behavior, and add a
clear error log or throw if neither projectId nor credentials are available so
startup fails fast with guidance for local setup.
- Around line 176-185: Replace fragile string matching with a custom error type:
add an AuthError class and have verifyAuth throw AuthError for any auth failures
(missing header, token expired, invalid signature, etc.), then in the catch
block in server/src/index.ts replace the message.includes('authorization') check
with an instanceof AuthError check to return 401 and otherwise fall through to
500; update any call sites of verifyAuth to ensure they propagate AuthError as
thrown.
- Around line 89-108: parseScheduleFromResponse assumes each parsed block has
valid string fields which can cause downstream runtime errors; validate each
object from JSON.parse inside parseScheduleFromResponse by checking that
startTime and endTime are non-empty strings matching a time pattern (e.g.,
HH:MM), label is a string, and color is a string (fallback to '#60a5fa' if
missing), filter out any blocks that fail validation, and return null or an
empty array if no valid blocks remain; reference the parseScheduleFromResponse
function and the mapped block shape (startTime, endTime, label, color) when
making these checks.
- Around line 150-160: The Anthropic API call (client.messages.create) currently
has no timeout and can hang; add a timeout option to the call (or when
constructing the Anthropic client) so requests fail fast—e.g., use a
configurable ANTHROPIC_TIMEOUT_MS env var with a sensible default (e.g., 30000
ms) and pass that value into the timeout option when calling
client.messages.create (or into new Anthropic({...})), ensuring the timeout
applies to the request that assigns to response and uses
claudeApiKey/model/SYSTEM_PROMPT/scheduleContext.
| const assistantText = | ||
| response.content[0].type === 'text' | ||
| ? response.content[0].text | ||
| : 'Sorry, I could not generate a response.'; |
There was a problem hiding this comment.
Accessing response.content[0] without checking array length could throw.
If the API returns an empty content array, accessing index 0 would result in undefined, and the type check would fail silently, returning the fallback message. However, if the structure changes, this could cause runtime errors.
🛡️ Proposed defensive check
- const assistantText =
- response.content[0].type === 'text'
- ? response.content[0].text
- : 'Sorry, I could not generate a response.';
+ const firstContent = response.content[0];
+ const assistantText =
+ firstContent?.type === 'text'
+ ? firstContent.text
+ : 'Sorry, I could not generate a response.';📝 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.
| const assistantText = | |
| response.content[0].type === 'text' | |
| ? response.content[0].text | |
| : 'Sorry, I could not generate a response.'; | |
| const firstContent = response.content[0]; | |
| const assistantText = | |
| firstContent?.type === 'text' | |
| ? firstContent.text | |
| : 'Sorry, I could not generate a response.'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/src/index.ts` around lines 162 - 165, The current assignment to
assistantText directly indexes response.content[0] (seen in the assistantText
declaration) which can throw or behave incorrectly if response.content is empty
or undefined; change the logic to first verify response.content exists and has
at least one item and that that item's type and text are defined (e.g., check
response.content?.length > 0 and response.content[0].type === 'text' and
response.content[0].text) before using it, otherwise use the fallback
string—update the assistantText computation to perform these defensive checks
(or use optional chaining and a safe default) so accessing response.content[0]
cannot cause a runtime error.
There was a problem hiding this comment.
🧹 Nitpick comments (4)
src/services/aiService.ts (1)
25-31: Setthis.namefor proper error identification.Custom error classes should set the
nameproperty for better debugging and error handling.♻️ Proposed fix
export class RateLimitError extends Error { code: string; constructor(message: string, code: string) { super(message); + this.name = 'RateLimitError'; this.code = code; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/aiService.ts` around lines 25 - 31, The custom error class RateLimitError doesn't set the error name; update the RateLimitError constructor to assign this.name = 'RateLimitError' (after calling super) so stack traces and instanceof checks properly identify the error; ensure the constructor still accepts (message: string, code: string) and preserves this.code.server/src/index.ts (2)
201-220: Consider validating AI-generated time blocks before returning.
parseScheduleFromResponsetrusts AI output without validation. Invalid time formats or non-5-minute increments could propagate to the client. A basic validation pass would improve robustness.🛡️ Suggested validation
function parseScheduleFromResponse(text: string): TimeBlock[] | null { const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/); if (!scheduleMatch) return null; try { const parsed = JSON.parse(scheduleMatch[1]); if (!Array.isArray(parsed)) return null; + const isValidTime = (t: string) => { + const m = /^(\d{2}):(\d{2})$/.exec(t); + if (!m) return false; + const hh = Number(m[1]), mm = Number(m[2]); + return hh >= 0 && hh <= 23 && mm >= 0 && mm < 60 && mm % 5 === 0; + }; + return parsed.map( (block: { startTime: string; endTime: string; label: string; color: string }) => ({ startTime: block.startTime, endTime: block.endTime, label: block.label, color: block.color || '#60a5fa', }) - ); + ).filter(b => isValidTime(b.startTime) && isValidTime(b.endTime)); } catch { return null; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 201 - 220, parseScheduleFromResponse currently trusts the AI JSON and returns blocks directly; add a validation pass to ensure each block's startTime and endTime are valid "HH:MM" strings, hours 0–23, minutes 0–59 and divisible by 5, and that startTime < endTime (or discard invalid blocks). Implement an isValidTime helper and use it to filter the mapped results (also preserve the existing color fallback logic) so the function returns only well-formed TimeBlock entries or null when none valid.
290-298: Input validation is incomplete fortimeBlocks.The endpoint validates
messagesis an array but doesn't validatetimeBlocks. If a malformedtimeBlocksis sent,formatScheduleContextcould produce unexpected output or errors.🛡️ Proposed validation
const { messages, timeBlocks } = req.body as { messages: ChatMessage[]; timeBlocks: TimeBlock[]; }; if (!messages || !Array.isArray(messages)) { res.status(400).json({ error: 'Messages array is required' }); return; } + + if (timeBlocks && !Array.isArray(timeBlocks)) { + res.status(400).json({ error: 'timeBlocks must be an array' }); + return; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/src/index.ts` around lines 290 - 298, The request currently validates messages but not timeBlocks, which can lead to errors when calling formatScheduleContext; add validation for req.body.timeBlocks similar to messages: ensure timeBlocks exists, is an array, and optionally verify each element matches the expected TimeBlock shape (e.g., has start/end or required fields) before calling formatScheduleContext; on failure return res.status(400).json({ error: 'timeBlocks array is required' }) (or a more specific message) so formatScheduleContext receives well-formed input.src/components/AIAssistant.tsx (1)
209-221: Consider replacingconfirm()with a custom modal.
window.confirmis a synchronous blocking call that can be jarring on mobile and doesn't match the app's visual design. A custom confirmation modal would provide a better user experience.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/AIAssistant.tsx` around lines 209 - 221, Replace the synchronous window.confirm call in handleApplySchedule with a non-blocking custom confirmation modal: introduce local state (e.g., showConfirmModal) and render a ConfirmModal (or your existing Modal component) that displays the same warning and has Confirm/Cancel actions; change handleApplySchedule to open the modal instead of calling confirm(), and move the onApplySchedule(blocks) call and setMessages update into the modal's Confirm handler so the schedule is applied only when the user confirms. Ensure the ConfirmModal exposes callbacks (onConfirm, onCancel) and that accessibility/focus management is handled by the modal component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@server/src/index.ts`:
- Around line 201-220: parseScheduleFromResponse currently trusts the AI JSON
and returns blocks directly; add a validation pass to ensure each block's
startTime and endTime are valid "HH:MM" strings, hours 0–23, minutes 0–59 and
divisible by 5, and that startTime < endTime (or discard invalid blocks).
Implement an isValidTime helper and use it to filter the mapped results (also
preserve the existing color fallback logic) so the function returns only
well-formed TimeBlock entries or null when none valid.
- Around line 290-298: The request currently validates messages but not
timeBlocks, which can lead to errors when calling formatScheduleContext; add
validation for req.body.timeBlocks similar to messages: ensure timeBlocks
exists, is an array, and optionally verify each element matches the expected
TimeBlock shape (e.g., has start/end or required fields) before calling
formatScheduleContext; on failure return res.status(400).json({ error:
'timeBlocks array is required' }) (or a more specific message) so
formatScheduleContext receives well-formed input.
In `@src/components/AIAssistant.tsx`:
- Around line 209-221: Replace the synchronous window.confirm call in
handleApplySchedule with a non-blocking custom confirmation modal: introduce
local state (e.g., showConfirmModal) and render a ConfirmModal (or your existing
Modal component) that displays the same warning and has Confirm/Cancel actions;
change handleApplySchedule to open the modal instead of calling confirm(), and
move the onApplySchedule(blocks) call and setMessages update into the modal's
Confirm handler so the schedule is applied only when the user confirms. Ensure
the ConfirmModal exposes callbacks (onConfirm, onCancel) and that
accessibility/focus management is handled by the modal component.
In `@src/services/aiService.ts`:
- Around line 25-31: The custom error class RateLimitError doesn't set the error
name; update the RateLimitError constructor to assign this.name =
'RateLimitError' (after calling super) so stack traces and instanceof checks
properly identify the error; ensure the constructor still accepts (message:
string, code: string) and preserves this.code.
Summary by CodeRabbit
New Features
UI Improvements
Documentation
Config