Skip to content

Conversation

@Ericzhou-ez
Copy link
Collaborator

@Ericzhou-ez Ericzhou-ez commented Oct 14, 2025

PR: Settings Modal and Personalization Instructions for Witely

Overview

This PR introduces a comprehensive settings modal with personalization features, enabling users to configure their preferences and provide personal information for tailored AI responses and auto-fill capabilities (when the browser extension gets built).

✨ Key Features

1. Settings Modal System

  • Full-featured settings interface accessible via Cmd/Ctrl + , keyboard shortcut
  • Responsive design with separate mobile and desktop layouts
  • Glassmorphic overlay with smooth animations powered by Framer Motion
  • Escape key to close modal
  • Automatic body scroll prevention when modal is open

2. Personalization Section

The centerpiece of this PR, allowing users to provide information that helps Witely deliver more personalized responses:

Personal Information

  • Name, email, phone
  • Complete address fields (line 1, line 2, city, state, zip code, country)
  • Gender
  • Edit/view toggle with inline editing
  • Optimized PATCH requests that only send changed fields

Bio Section

  • Free-form text area for users to describe themselves
  • Helps AI understand user context and preferences
  • Separate edit/save flow

Privacy Transparency

  • Clear disclaimer explaining data usage
  • Tooltip with detailed information about why Witely needs this data
  • Emphasis on privacy and user control

3. Additional Settings Sections

  • General: Application-wide preferences
  • Account: User account management
  • Security: Security and privacy settings
  • Notifications: Notification preferences
  • Apps: Integration settings

🏗️ Technical Implementation

New API Routes

GET /api/personalization

Fetches user's personalization data

  • Returns 404 if no data exists (first time user)
  • Returns complete personalization object

PATCH /api/personalization/personal-information

Updates personal information fields

  • Only processes changed fields for efficiency
  • Validates and sanitizes input
  • Returns updated personalization data

POST /api/personalization/bio

Updates user bio

  • Accepts bio text
  • Stores in database
  • Returns success confirmation

GET /api/user

Fetches current user information

  • Used by the use-user hook
  • Provides user context throughout the app

Database Changes

Migration 0011 (0011_clear_randall.sql)

Adds personalization fields to user table:

ALTER TABLE "User" ADD COLUMN "bio" text;
-- Additional personalization fields

Migration 0012 (0012_mighty_sumo.sql)

Additional schema updates for personalization support

New Components

Core Components

  • components/settings-modal.tsx - Main modal container with keyboard shortcuts
  • components/settings/settings-desktop-layout.tsx - Desktop layout with sidebar navigation
  • components/settings/settings-mobile-layout.tsx - Mobile-optimized layout
  • components/settings/settings-data.tsx - Settings configuration and data structure

Section Components

  • components/settings/sections/general-section.tsx
  • components/settings/sections/account-section.tsx
  • components/settings/sections/security-section.tsx
  • components/settings/sections/notifications-section.tsx
  • components/settings/sections/personalization-section.tsx - Main personalization interface
  • components/settings/sections/apps-section.tsx

Personalization Subcomponents

  • bio-display.tsx - Display mode for bio
  • bio-edit.tsx - Edit mode for bio with save/cancel
  • personal-info-display.tsx - Display mode for personal information
  • personal-info-edit.tsx - Edit mode with form inputs

New UI Components

  • components/ui/switch.tsx - Toggle switch component
  • components/ui/kbd.tsx - Keyboard shortcut display component

New Hooks

hooks/use-settings-modal.ts

Zustand store for managing settings modal state:

  • isOpen - Modal visibility state
  • activeSection - Currently active settings section
  • open() - Open modal
  • close() - Close modal
  • toggle() - Toggle modal state
  • setActiveSection() - Navigate between sections

hooks/use-user.ts

SWR-powered hook for fetching and caching user data:

  • Automatic revalidation
  • Error handling
  • Loading states
  • Cached user information

Database Schema Updates

New fields in User table (lib/db/schema.ts):

// Personalization fields
bio: text('bio'),
name: text('name'),
email: text('email'),
phone: text('phone'),
addressLine1: text('addressLine1'),
addressLine2: text('addressLine2'),
city: text('city'),
state: text('state'),
zipCode: text('zipCode'),
country: text('country'),
gender: text('gender'),

New type definition (lib/db/types.ts):

export type Personalization = {
  bio: string | null;
  name: string | null;
  email: string | null;
  phone: string | null;
  addressLine1: string | null;
  addressLine2: string | null;
  city: string | null;
  state: string | null;
  zipCode: string | null;
  country: string | null;
  gender: string | null;
};

Enhanced Error Handling

lib/errors.ts has been expanded with new error types:

  • Improved error messages for API failures
  • Better user-facing error descriptions
  • Structured error handling for personalization operations

🎨 UI/UX Highlights

Animations

  • Smooth modal enter/exit animations using Framer Motion
  • Backdrop blur effect for modern glassmorphic design
  • Spring animations with easing curves: [0.16, 1, 0.3, 1]

Responsive Design

  • Separate mobile and desktop layouts
  • Touch-optimized interface for mobile users
  • Keyboard navigation support on desktop

Form Handling

  • Inline editing with edit/cancel/save controls
  • Loading states during save operations
  • Success/error toast notifications
  • Only sends changed fields to reduce network overhead

Accessibility

  • Keyboard shortcuts (Cmd/Ctrl + ,)
  • Escape key to close
  • Tooltips with detailed explanations
  • Clear visual hierarchy

🔄 Integration Points

Updated Components

components/app-sidebar.tsx

  • Added settings button to open modal
  • Integrated keyboard shortcut indicator

components/user-menu.tsx

  • Added settings menu item
  • Connected to settings modal hook

app/layout.tsx

  • Included SettingsModal component in root layout
  • Ensures modal is available globally

app/globals.css

  • New styles for settings UI
  • Toast notification styles
  • Form input enhancements

📊 Use Cases

1. Personalized AI Responses

Users can provide background information that helps Witely:

  • Understand user context (profession, interests, location)
  • Tailor responses to user's knowledge level
  • Remember user preferences across sessions

2. Auto-Fill Assistance

When browsing websites that require form filling:

  • Witely can auto-suggest saved personal information
  • Speeds up checkout processes
  • Reduces repetitive data entry

3. User Profile Management

  • Central location for managing personal data
  • Easy to update information as it changes
  • Clear visibility into what data is stored

🧪 Testing Recommendations

Manual Testing

  1. Settings Modal

    • Open modal with Cmd/Ctrl + ,
    • Close modal with Escape
    • Navigate between sections
    • Verify responsive behavior on mobile
  2. Personalization

    • Enter personal information
    • Save and verify data persists
    • Edit existing information
    • Cancel editing and verify no changes saved
    • Test with empty/partial data
  3. API Endpoints

    • GET personalization (existing user)
    • GET personalization (new user - 404)
    • PATCH personal information
    • POST bio update

Automated Testing Considerations

  • E2E tests for settings modal flow
  • Unit tests for personalization API routes
  • Component tests for form validation
  • Integration tests for database operations

🔐 Security & Privacy

Data Handling

  • All personalization data stored securely in database
  • No third-party sharing
  • User has full control over their data
  • Clear explanations of data usage

Privacy Considerations

  • Transparent disclaimer in UI
  • Optional fields - users can provide as much or as little as they want
  • Data only used to improve AI responses and auto-fill
  • Future consideration: Add data export/deletion options

📦 Dependencies Updated

New Dependencies

  • Added framer-motion for animations (if not already present)
  • SWR hooks for data fetching (use-user)
  • Zustand for state management (use-settings-modal)

Package Updates

See package.json and pnpm-lock.yaml for specific version changes.

@vercel
Copy link

vercel bot commented Oct 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
witely Ready Ready Preview Comment Oct 15, 2025 5:28pm

@gemini-code-assist
Copy link

Summary of Changes

Hello @Ericzhou-ez, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a robust settings modal for Witely, empowering users to customize their experience through detailed personalization options. By allowing users to provide personal information and a bio, the system can offer more relevant AI responses and prepare for future auto-fill functionalities. The implementation includes a modular UI, new API endpoints for data management, and corresponding database schema updates, all designed with responsiveness, accessibility, and privacy in mind.

Highlights

  • Settings Modal System: Introduced a comprehensive settings modal accessible via Cmd/Ctrl + , shortcut, featuring a responsive design, glassmorphic animations with Framer Motion, and automatic body scroll prevention.
  • Personalization Section: Added a central personalization interface allowing users to input personal information (name, email, address, gender) and a free-form bio. This data is used for tailored AI responses and future auto-fill capabilities, with a clear privacy disclaimer.
  • Modular Settings Sections: Implemented dedicated sections for General, Account, Security, Notifications, and Apps settings, providing a structured and extensible settings experience.
  • New API Endpoints: Created new API routes for fetching user personalization data (GET /api/personalization), updating personal information (PATCH /api/personalization/personal-information), updating user bio (POST /api/personalization/bio), and fetching general user details (GET /api/user).
  • Database Schema Updates: Introduced new database migrations to create a Personalization table (initially PersonalInformation) to store user-specific data like bio and personal information (stored as JSON), linked to the User table.
  • New UI Components and Hooks: Developed several new React components for the settings UI (e.g., SettingsModal, SettingsDesktopLayout, SettingsMobileLayout, BioDisplay, PersonalInfoEdit) and generic UI elements like Switch and Kbd. New hooks use-settings-modal (Zustand) and use-user (SWR) were added for state and data management.
  • Enhanced Error Handling: Expanded the error handling system with a new WitelyError class and added 'personalization' as a recognized error surface for more granular error reporting.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Dashboard-User-Id: 4a95fa0f-db5a-4a0d-b46b-558f51207d70
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive settings modal and personalization features, including new API routes, database migrations, UI components, and hooks. The implementation is extensive and well-structured. My review focuses on enhancing the robustness and maintainability of the new features. I've provided suggestions to improve error handling in the API routes, correct state management logic in the personalization forms to handle cancellation gracefully, increase code clarity, and ensure better type safety and consistency across the data layer.

Comment on lines 29 to 35
} catch (error) {
console.error("Error posting user bio", error);
return NextResponse.json(
{ error: "Failed to post user bio" },
{ status: 500 }
);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current error handling is very generic. It would be beneficial to specifically handle validation errors from Zod. This will provide more meaningful feedback to the client in case of invalid input, returning a 400 Bad Request with validation details instead of a generic 500 Internal Server Error.

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ error: "Invalid input", issues: error.errors }, { status: 400 });
    }
    console.error("Error posting user bio", error);
    return NextResponse.json(
      { error: "Failed to post user bio" },
      { status: 500 }
    );
  }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist suggest a way of catching zod errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already provided a suggestion for catching Zod errors in my previous comment. The suggested code block demonstrates how to use instanceof z.ZodError to catch validation errors and return a 400 Bad Request with the validation issues. Please refer to the code suggestion in my initial comment for the implementation.

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ error: "Invalid input", issues: error.errors }, { status: 400 });
    }
    console.error("Error posting user bio", error);
    return NextResponse.json(
      { error: "Failed to post user bio" },
      { status: 500 }
    );
  }
Suggested change
} catch (error) {
console.error("Error posting user bio", error);
return NextResponse.json(
{ error: "Failed to post user bio" },
{ status: 500 }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json({ error: "Invalid input", issues: error.errors }, { status: 400 });
}
console.error("Error posting user bio", error);
return NextResponse.json(
{ error: "Failed to post user bio" },
{ status: 500 }
);

Comment on lines 53 to 59
} catch (error) {
console.error("Error patching personal information", error);
return NextResponse.json(
{ error: "Failed to patch personal information" },
{ status: 500 }
);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the other new API routes, the error handling here is generic. It's a good practice to specifically catch Zod validation errors and return a 400 Bad Request with details about the validation failure. This provides better feedback to the frontend and makes debugging easier.

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ error: "Invalid input", issues: error.errors }, { status: 400 });
    }
    console.error("Error patching personal information", error);
    return NextResponse.json(
      { error: "Failed to patch personal information" },
      { status: 500 }
    );
  }

const router = useRouter();

const handleCopyUUID = async () => {
if (session?.user?.id) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The condition if (session?.user?.id) seems inconsistent with how user data is handled in this component. The useUser hook provides the complete user object, which includes the ID. It would be more consistent and robust to check for user?.id instead, as the session object from next-auth might not always contain the database user ID, depending on the session strategy.

Suggested change
if (session?.user?.id) {
if (user?.id) {

Comment on lines 77 to 92
style={{
backgroundColor:
color.value === "monochrome"
? "#808080"
: color.value === "orange"
? "#f97316"
: color.value === "blue"
? "#3b82f6"
: color.value === "green"
? "#22c55e"
: color.value === "purple"
? "#a855f7"
: color.value === "pink"
? "#ec4899"
: "#808080",
}}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a large ternary operator within the style attribute for setting the background color can be hard to read and maintain. Consider using a map object to store the color values. This will make the code cleaner and easier to extend with new colors in the future.

Also, note that Tailwind's JIT compiler cannot generate classes from dynamic strings like bg-${color.value}-500, so using inline styles is the correct approach here.

                      style={{
                        backgroundColor: {
                          monochrome: "#808080",
                          orange: "#f97316",
                          blue: "#3b82f6",
                          green: "#22c55e",
                          purple: "#a855f7",
                          pink: "#ec4899",
                        }[color.value] ?? "#808080",
                      }}

Comment on lines 214 to 216
const handleCancelPersonalInfo = useCallback(() => {
setIsEditingPersonalInfo(false);
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the user cancels editing their personal information, the changes they've made in the form are not reverted. If they open the edit form again, they will see their unsaved changes. To improve the user experience, the form state should be reset to the original values when the cancel button is clicked.

  const handleCancelPersonalInfo = useCallback(() => {
    setName(originalValues.name);
    setEmail(originalValues.email);
    setPhone(originalValues.phone);
    setAddressLine1(originalValues.addressLine1);
    setAddressLine2(originalValues.addressLine2);
    setCity(originalValues.city);
    setState(originalValues.state);
    setZipCode(originalValues.zipCode);
    setCountry(originalValues.country);
    setGender(originalValues.gender);
    setIsEditingPersonalInfo(false);
  }, [originalValues]);

Comment on lines 249 to 251
const handleCancelBio = useCallback(() => {
setIsEditingBio(false);
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When a user cancels editing their bio, the changes are not reverted because the original value isn't stored. This can lead to a confusing user experience.

To fix this, you should:

  1. Create a new state to hold the original bio value at the top of your component:
const [originalBio, setOriginalBio] = useState("");
  1. Populate this state when the personalization data is fetched in useEffect:
// inside useEffect, after fetching data
setBio(personalization.bio || "");
setOriginalBio(personalization.bio || "");
  1. In handleCancelBio, use this original value to reset the bio state:
const handleCancelBio = useCallback(() => {
  setBio(originalBio);
  setIsEditingBio(false);
}, [originalBio]);

} catch (_error) {
throw new WitelyError(
"bad_request:personalization",
"Failed to update personal information by user id"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There seems to be a copy-paste error in this error message. It should refer to 'bio' instead of 'personal information' to avoid confusion during debugging.

Suggested change
"Failed to update personal information by user id"
"Failed to update bio by user id"

Comment on lines 8 to 23
export type PersonalInformation = {
name: string | undefined;
email: string | undefined;
phone: string | undefined;
addressLine1: string | undefined;
addressLine2: string | undefined;
city: string | undefined;
state: string | undefined;
zipCode: string | undefined;
country: string | undefined;
gender: Gender | undefined;
};

type Bio = string;

export type Personalization = PersonalInformation & { bio: Bio };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The types defined here don't fully align with the database schema, which can lead to type-related issues. The database columns for personalization can be null.

  1. In PersonalInformation, the fields are typed as string | undefined, but they should be string | null to accurately reflect the database schema.
  2. Bio is typed as string, but it can also be null in the database. It should be string | null.
  3. Personalization inherits from PersonalInformation and adds bio, but the bio type is also incorrect here.
Suggested change
export type PersonalInformation = {
name: string | undefined;
email: string | undefined;
phone: string | undefined;
addressLine1: string | undefined;
addressLine2: string | undefined;
city: string | undefined;
state: string | undefined;
zipCode: string | undefined;
country: string | undefined;
gender: Gender | undefined;
};
type Bio = string;
export type Personalization = PersonalInformation & { bio: Bio };
export type PersonalInformation = {
name: string | null;
email: string | null;
phone: string | null;
addressLine1: string | null;
addressLine2: string | null;
city: string | null;
state: string | null;
zipCode: string | null;
country: string | null;
gender: Gender | null;
};
type Bio = string | null;
export type Personalization = PersonalInformation & { bio: Bio };

Comment on lines 80 to 118
export class WitelyError extends Error {
type: ErrorType;
surface: Surface;
statusCode: number;

constructor(errorCode: ErrorCode, cause?: string) {
super();

const [type, surface] = errorCode.split(":");

this.type = type as ErrorType;
this.cause = cause;
this.surface = surface as Surface;
this.message = getMessageByErrorCode(errorCode);
this.statusCode = getStatusCodeByType(this.type);
}

toResponse() {
const code: ErrorCode = `${this.type}:${this.surface}`;
const visibility = visibilityBySurface[this.surface];

const { message, cause, statusCode } = this;

if (visibility === "log") {
console.error({
code,
message,
cause,
});

return Response.json(
{ code: "", message: "Something went wrong. Please try again later." },
{ status: statusCode }
);
}

return Response.json({ code, message, cause }, { status: statusCode });
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The introduction of the WitelyError class alongside the existing ChatSDKError creates an inconsistency in error handling. Some new functions in lib/db/queries.ts use WitelyError, while older ones still use ChatSDKError.

For better maintainability, it would be best to unify this. You could either:

  1. Rename ChatSDKError to WitelyError and use it everywhere.
  2. Refactor all existing error handling to use the new WitelyError class.

Having a single, consistent error handling class will make the codebase easier to understand and maintain.

@paragon-review
Copy link

✅ Optimization complete for commit 82e7c00.

A new optimization PR is available: #10 targeting settings.

@paragon-review paragon-review bot mentioned this pull request Oct 15, 2025
@Ericzhou-ez
Copy link
Collaborator Author

When a new user is created, create the partial personalization table for them.

Populate with name and email.

This set name and email as not being able to be unable

@Ericzhou-ez
Copy link
Collaborator Author

@gemini-code-assist review PR again and assess merge readiness.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive and comprehensive pull request that introduces a major new feature: a settings modal with personalization capabilities. The code is well-structured, leveraging modern React patterns with hooks like useSWR for data fetching and Zustand for global state management, which is excellent. The new components are well-organized, and the API routes are clearly defined. My review focuses on a critical data integrity issue in the new database schema, opportunities for refactoring to improve code maintainability and efficiency, and some refinements to input validation for a better user experience. Addressing these points will help ensure the new features are robust and scalable.

Comment on lines +1 to +6
CREATE TABLE IF NOT EXISTS "PersonalInformation" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid NOT NULL,
"information" json,
"bio" varchar(500)
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The PersonalInformation table (later renamed to Personalization) is missing a UNIQUE constraint on the userId column. This is a critical issue as it could allow multiple personalization records to be created for a single user, leading to data integrity problems.

The application code seems to assume a one-to-one relationship (e.g., by accessing records[0]), so enforcing this at the database level is essential. Please add a UNIQUE constraint to the userId column.

CREATE TABLE IF NOT EXISTS "PersonalInformation" (
	"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
	"userId" uuid NOT NULL UNIQUE,
	"information" json,
	"bio" varchar(500)
);

Comment on lines 670 to 697
export async function updateBioByUserId({
userId,
bio,
}: {
userId: string;
bio: string;
}) {
try {
const existing = await db
.select()
.from(personalization)
.where(eq(personalization.userId, userId));

if (existing.length > 0) {
return await db
.update(personalization)
.set({ bio })
.where(eq(personalization.userId, userId));
}

return db.insert(personalization).values({ userId, bio });
} catch (_error) {
throw new WitelyError(
"bad_request:personalization",
"Failed to update bio by user id"
);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of updateBioByUserId uses a SELECT followed by an UPDATE or INSERT. This approach is not atomic and can be less efficient.

Assuming a UNIQUE constraint is added to the userId column in the Personalization table (as suggested in another comment), you can simplify this logic into a single, more efficient "upsert" operation using Drizzle's onConflictDoUpdate feature. This is a more robust and idiomatic way to handle this in Postgres.

export async function updateBioByUserId({
  userId,
  bio,
}: {
  userId: string;
  bio: string;
}) {
  try {
    return await db
      .insert(personalization)
      .values({ userId, bio })
      .onConflictDoUpdate({ target: personalization.userId, set: { bio } });
  } catch (_error) {
    throw new WitelyError(
      "bad_request:personalization",
      "Failed to update bio by user id"
    );
  }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist even better just update because the personalization table is now guaranteed to exist (created when a user creates their account).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right, Ericzhou-ez! Given that the personalization record is now guaranteed to exist when a user creates their account, a simple UPDATE operation is indeed the most efficient and direct approach. My previous suggestion for onConflictDoUpdate was based on the assumption that the record might not exist, but your clarification simplifies the logic further.

Here's the updated suggestion for updateBioByUserId:

Suggested change
export async function updateBioByUserId({
userId,
bio,
}: {
userId: string;
bio: string;
}) {
try {
const existing = await db
.select()
.from(personalization)
.where(eq(personalization.userId, userId));
if (existing.length > 0) {
return await db
.update(personalization)
.set({ bio })
.where(eq(personalization.userId, userId));
}
return db.insert(personalization).values({ userId, bio });
} catch (_error) {
throw new WitelyError(
"bad_request:personalization",
"Failed to update bio by user id"
);
}
}
export async function updateBioByUserId({
userId,
bio,
}: {
userId: string;
bio: string;
}) {
try {
return await db
.update(personalization)
.set({ bio })
.where(eq(personalization.userId, userId));
} catch (_error) {
throw new WitelyError(
"bad_request:personalization",
"Failed to update bio by user id"
);
}
}

Comment on lines 12 to 13
name: z.string().min(5).max(100).optional(),
email: z.string().email().min(6).max(255).optional(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validation for name and email seems a bit too strict.

  • name: Requiring a minimum of 5 characters might exclude valid names (e.g., "Li", "An", "Jo"). It's generally better to have a lower minimum, like 1 or 2, or remove it entirely if not strictly necessary.
  • email: The min(6) rule is also a bit arbitrary. While most emails will be longer, it's safer to rely on the .email() validation for correctness and just have a max() length constraint.

I'd recommend relaxing these minimum length constraints to avoid rejecting valid user input.

Suggested change
name: z.string().min(5).max(100).optional(),
email: z.string().email().min(6).max(255).optional(),
name: z.string().min(2).max(100).optional(),
email: z.string().email().max(255).optional(),

Comment on lines 23 to 48
// Personal info states
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [addressLine1, setAddressLine1] = useState("");
const [addressLine2, setAddressLine2] = useState("");
const [city, setCity] = useState("");
const [state, setState] = useState("");
const [zipCode, setZipCode] = useState("");
const [country, setCountry] = useState("");
const [gender, setGender] = useState("");
const [isEditingPersonalInfo, setIsEditingPersonalInfo] = useState(false);

// Original values for tracking changes
const [originalValues, setOriginalValues] = useState({
name: "",
email: "",
phone: "",
addressLine1: "",
addressLine2: "",
city: "",
state: "",
zipCode: "",
country: "",
gender: "",
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component uses a large number of useState hooks to manage the form state for personal information. While this works, it can become difficult to manage, especially when dealing with logic for resetting the form or tracking changes. The long dependency array in handleSavePersonalInfo is a symptom of this.

Consider using the useReducer hook to consolidate state management. This will centralize your update logic, make it easier to handle complex state transitions (like resetting the form), and simplify your callback dependencies.

Comment on lines 80 to 118
export class WitelyError extends Error {
type: ErrorType;
surface: Surface;
statusCode: number;

constructor(errorCode: ErrorCode, cause?: string) {
super();

const [type, surface] = errorCode.split(":");

this.type = type as ErrorType;
this.cause = cause;
this.surface = surface as Surface;
this.message = getMessageByErrorCode(errorCode);
this.statusCode = getStatusCodeByType(this.type);
}

toResponse() {
const code: ErrorCode = `${this.type}:${this.surface}`;
const visibility = visibilityBySurface[this.surface];

const { message, cause, statusCode } = this;

if (visibility === "log") {
console.error({
code,
message,
cause,
});

return Response.json(
{ code: "", message: "Something went wrong. Please try again later." },
{ status: statusCode }
);
}

return Response.json({ code, message, cause }, { status: statusCode });
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new WitelyError class is nearly identical to the existing ChatSDKError class. This code duplication violates the DRY (Don't Repeat Yourself) principle and can make future maintenance more difficult, as changes would need to be applied in two places.

To improve this, I recommend removing the WitelyError class and using ChatSDKError for all application-specific errors. If needed, you could rename ChatSDKError to a more generic name like AppError to better reflect its broader usage.

…ied WitelyError in lieu of generic ChatSDK error
@Ericzhou-ez
Copy link
Collaborator Author

@gemini-code-assist check again ive unified error handling and added the unique constraint on the userId foreign key in personalization table.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major new feature: a comprehensive settings modal with personalization options. The implementation is extensive, adding new API routes, database migrations, UI components, and hooks. The code is generally well-structured, especially the new React components which are nicely broken down and memoized. My review focuses on improving consistency in error handling, refining state management in React components, and fixing a potential typing issue in the database types. I've also pointed out a minor typo in user-facing metadata.

Comment on lines +21 to +23
type Bio = string;

export type Personalization = PersonalInformation & { bio: Bio };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Bio type is defined as string, but the database schema (lib/db/schema.ts) allows the bio column in the Personalization table to be null. This type mismatch can lead to runtime errors if null is encountered. The Personalization type, which composes Bio, should also reflect that bio can be null.

Suggested change
type Bio = string;
export type Personalization = PersonalInformation & { bio: Bio };
type Bio = string | null;
export type Personalization = PersonalInformation & { bio: Bio };

Comment on lines +9 to +11
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other API routes in the application, it's better to use the custom WitelyError class for handling errors. This centralizes error responses and ensures a uniform API error structure. This suggestion also applies to other new API routes in this PR.

      return new WitelyError("unauthorized:auth").toResponse();

Comment on lines +30 to +32
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other API routes in the application, it's better to use the custom WitelyError class for handling errors. This centralizes error responses and ensures a uniform API error structure. This suggestion also applies to other new API routes in this PR.

      return new WitelyError("unauthorized:auth").toResponse();

Comment on lines +36 to +38
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To maintain consistent error handling across the API, this error should be thrown using the custom WitelyError class. This approach is used in other refactored parts of the application.

      return new WitelyError("not_found:database", "User not found").toResponse();

Comment on lines +9 to +11
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other API routes in the application, it's better to use the custom WitelyError class for handling errors. This centralizes error responses and ensures a uniform API error structure. This suggestion also applies to other new API routes in this PR.

      return new WitelyError("unauthorized:auth").toResponse();

Comment on lines +13 to +14
description:
"Never Copy & Paste Again; Witely Gives You the Anwsers You Need, Before You Ask",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a typo in the application's description metadata. "Anwsers" should be "Answers".

Suggested change
description:
"Never Copy & Paste Again; Witely Gives You the Anwsers You Need, Before You Ask",
description:
"Never Copy & Paste Again; Witely Gives You the Answers You Need, Before You Ask",

Comment on lines +11 to +30
const { isOpen, close } = useSettingsModal();
const isMobile = useIsMobile();

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd/Ctrl + , to open settings
if ((e.metaKey || e.ctrlKey) && e.key === ",") {
e.preventDefault();
useSettingsModal.getState().toggle();
}
// Escape to close
if (e.key === "Escape" && isOpen) {
e.preventDefault();
close();
}
};

window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, close]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The useEffect for handling keydown events can be simplified. Instead of calling useSettingsModal.getState().toggle(), you can get the toggle function from the useSettingsModal hook and include it in the dependency array. This makes the component's dependencies more explicit and follows a more standard React pattern.

Suggested change
const { isOpen, close } = useSettingsModal();
const isMobile = useIsMobile();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd/Ctrl + , to open settings
if ((e.metaKey || e.ctrlKey) && e.key === ",") {
e.preventDefault();
useSettingsModal.getState().toggle();
}
// Escape to close
if (e.key === "Escape" && isOpen) {
e.preventDefault();
close();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, close]);
const { isOpen, close, toggle } = useSettingsModal();
const isMobile = useIsMobile();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd/Ctrl + , to open settings
if ((e.metaKey || e.ctrlKey) && e.key === ",") {
e.preventDefault();
toggle();
}
// Escape to close
if (e.key === "Escape" && isOpen) {
e.preventDefault();
close();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isOpen, close, toggle]);

Comment on lines 29 to 33
useEffect(() => {
if (copied && user?.id) {
toast({ type: "success", description: "Copied user uuid to clipboard!" });
}
}, [copied, user?.id]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This useEffect is not necessary. The toast notification can be triggered directly inside the handleCopyUUID function after the clipboard operation is successful. This simplifies the code by removing a state dependency and an effect, and makes the logic more direct.

Comment on lines 24 to 48
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [addressLine1, setAddressLine1] = useState("");
const [addressLine2, setAddressLine2] = useState("");
const [city, setCity] = useState("");
const [state, setState] = useState("");
const [zipCode, setZipCode] = useState("");
const [country, setCountry] = useState("");
const [gender, setGender] = useState("");
const [isEditingPersonalInfo, setIsEditingPersonalInfo] = useState(false);

// Original values for tracking changes
const [originalValues, setOriginalValues] = useState({
name: "",
email: "",
phone: "",
addressLine1: "",
addressLine2: "",
city: "",
state: "",
zipCode: "",
country: "",
gender: "",
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component uses many useState hooks to manage the form state for personal information. For a complex form like this, using a useReducer hook would be beneficial. It would centralize the state update logic, make the handleCancelPersonalInfo function simpler, and improve the overall maintainability and readability of the component.

Comment on lines +20 to +22
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To maintain consistent error handling across the API, this error should be thrown using the custom WitelyError class. This approach is used in other refactored parts of the application.

      return new WitelyError("not_found:database", "User not found").toResponse();

@Ericzhou-ez Ericzhou-ez merged commit 56ffb2c into main Oct 15, 2025
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants