Repository: skalingclouds/ai-chatbot
Pull request #6: Add database URL fallback
Author: login: skalingclouds
Labels: name: codex
Created: 2025-05-18T03:41:29Z | Merged: 2025-05-18T03:43:42Z

## Summary
- support `DATABASE_URL` in migration and query helpers
- document `DATABASE_URL` fallback in README and `.env.example`
- include router and session in auth page effects

## Testing
- `pnpm lint` *(fails: connect EHOSTUNREACH)*
- `pnpm test` *(fails: connect EHOSTUNREACH)*

## Summary by Sourcery

Enable fallback to `DATABASE_URL` across all database configurations and migrations, and enrich chat interactions by integrating Danckuts API tools and corresponding UI components for user profiles, location options, and appointment management.

New Features:
- Support `DATABASE_URL` fallback for database connections in helpers, migrations, queries, and configuration.
- Add Danckuts API client library with endpoints for user profiles, locations, phone verification, and appointment operations.
- Introduce AI tools for Danckuts operations in the chat API and include them in the streaming toolset.
- Create new chat UI components (OptionButtons, AvailabilityDisplay, ProfileCard) to render tool outputs and enable user selections within the chat interface.

Bug Fixes:
- Add missing `router` and `updateSession` dependencies in the authentication page effects.

Enhancements:
- Propagate the `append` helper through chat components and update rendering logic to handle dynamic inputs from the new tools.

Documentation:
- Document the `DATABASE_URL` fallback in README and update `.env.example`.


Diff:
diff --git a/.env.example b/.env.example
index 6a40fdd61a..dce9e10336 100644
--- a/.env.example
+++ b/.env.example
@@ -11,9 +11,16 @@ XAI_API_KEY=****
 BLOB_READ_WRITE_TOKEN=****
 
 # Instructions to create a PostgreSQL database here: https://vercel.com/docs/storage/vercel-postgres/quickstart
+# POSTGRES_URL is used locally, but DATABASE_URL also works (e.g. on Vercel)
 POSTGRES_URL=****
+DATABASE_URL=****
 
 
 # Instructions to create a Redis store here:
 # https://vercel.com/docs/redis
 REDIS_URL=****
+
+# Danckuts API configuration
+DANCKUTS_API_BASE_URL=http://atlas.danckuts.com
+DANCKUTS_API_TOKEN=****
+
diff --git a/README.md b/README.md
index b8aff09779..07600f72db 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,8 @@ You will need to use the environment variables [defined in `.env.example`](.env.
 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
 3. Download your environment variables: `vercel env pull`
 
+When deploying to Vercel, the database connection string may be provided as `DATABASE_URL`. Locally, the project expects `POSTGRES_URL`, but it will also fall back to `DATABASE_URL` if `POSTGRES_URL` is missing.
+
 ```bash
 pnpm install
 pnpm dev
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index 33e9e82709..e5be52a9ce 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -42,7 +42,7 @@ export default function Page() {
       updateSession();
       router.refresh();
     }
-  }, [state.status]);
+  }, [state.status, router, updateSession]);
 
   const handleSubmit = (formData: FormData) => {
     setEmail(formData.get('email') as string);
diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx
index ab2ee82667..bc33c7c3ac 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/(auth)/register/page.tsx
@@ -43,7 +43,7 @@ export default function Page() {
       updateSession();
       router.refresh();
     }
-  }, [state]);
+  }, [state.status, router, updateSession]);
 
   const handleSubmit = (formData: FormData) => {
     setEmail(formData.get('email') as string);
diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index 1526e964f3..28ba3a5da0 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -23,6 +23,7 @@ import { createDocument } from '@/lib/ai/tools/create-document';
 import { updateDocument } from '@/lib/ai/tools/update-document';
 import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
 import { getWeather } from '@/lib/ai/tools/get-weather';
+import { danckutsTools } from '@/lib/ai/tools/danckuts';
 import { isProductionEnvironment } from '@/lib/constants';
 import { myProvider } from '@/lib/ai/providers';
 import { entitlementsByUserType } from '@/lib/ai/entitlements';
@@ -159,6 +160,17 @@ export async function POST(request: Request) {
                   'createDocument',
                   'updateDocument',
                   'requestSuggestions',
+                  'getUserProfile',
+                  'getAllLocations',
+                  'checkPhoneNumberHasUser',
+                  'sendTextCode',
+                  'verifyTextCode',
+                  'getRecentAppointments',
+                  'cancelAppointment',
+                  'getAppointmentAvailability',
+                  'rescheduleAppointment',
+                  'scheduleAppointment',
+                  'showOptions',
                 ],
           experimental_transform: smoothStream({ chunking: 'word' }),
           experimental_generateMessageId: generateUUID,
@@ -170,6 +182,7 @@ export async function POST(request: Request) {
               session,
               dataStream,
             }),
+            ...danckutsTools,
           },
           onFinish: async ({ response }) => {
             if (session.user?.id) {
diff --git a/components/availability.tsx b/components/availability.tsx
new file mode 100644
index 0000000000..6d3bbb2811
--- /dev/null
+++ b/components/availability.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { format } from 'date-fns';
+import { OptionButtons } from './option-buttons';
+
+interface Availability {
+  dates: Record<string, string[]>;
+  slots: Record<string, Record<string, number>>;
+}
+
+export function AvailabilityDisplay({
+  availability,
+  onSelect,
+}: {
+  availability: Availability;
+  onSelect: (iso: string) => void;
+}) {
+  return (
+    <div className="flex flex-col gap-4">
+      {Object.entries(availability.slots).map(([date, times]) => (
+        <div key={date} className="flex flex-col gap-2">
+          <div className="font-medium">
+            {format(new Date(date), 'PPP')}
+          </div>
+          <OptionButtons
+            options={Object.keys(times).map((iso) =>
+              format(new Date(iso), 'p'),
+            )}
+            onSelect={(label) => {
+              const iso = Object.keys(times).find(
+                (t) => format(new Date(t), 'p') === label,
+              );
+              if (iso) onSelect(iso);
+            }}
+          />
+        </div>
+      ))}
+    </div>
+  );
+}
diff --git a/components/chat.tsx b/components/chat.tsx
index 9108772311..b9482fe932 100644
--- a/components/chat.tsx
+++ b/components/chat.tsx
@@ -134,6 +134,7 @@ export function Chat({
           messages={messages}
           setMessages={setMessages}
           reload={reload}
+          append={append}
           isReadonly={isReadonly}
           isArtifactVisible={isArtifactVisible}
         />
diff --git a/components/message.tsx b/components/message.tsx
index 7661d14f87..ff2d2d2f30 100644
--- a/components/message.tsx
+++ b/components/message.tsx
@@ -11,6 +11,9 @@ import { Markdown } from './markdown';
 import { MessageActions } from './message-actions';
 import { PreviewAttachment } from './preview-attachment';
 import { Weather } from './weather';
+import { OptionButtons } from './option-buttons';
+import { AvailabilityDisplay } from './availability';
+import { ProfileCard } from './profile-card';
 import equal from 'fast-deep-equal';
 import { cn, sanitizeText } from '@/lib/utils';
 import { Button } from './ui/button';
@@ -27,6 +30,7 @@ const PurePreviewMessage = ({
   isLoading,
   setMessages,
   reload,
+  append,
   isReadonly,
   requiresScrollPadding,
 }: {
@@ -36,6 +40,7 @@ const PurePreviewMessage = ({
   isLoading: boolean;
   setMessages: UseChatHelpers['setMessages'];
   reload: UseChatHelpers['reload'];
+  append: UseChatHelpers['append'];
   isReadonly: boolean;
   requiresScrollPadding: boolean;
 }) => {
@@ -212,6 +217,31 @@ const PurePreviewMessage = ({
                           result={result}
                           isReadonly={isReadonly}
                         />
+                      ) : toolName === 'showOptions' ? (
+                        <OptionButtons
+                          options={result.options}
+                          onSelect={(value) =>
+                            append({ role: 'user', content: value })
+                          }
+                        />
+                      ) : toolName === 'getAllLocations' ? (
+                        <OptionButtons
+                          options={result.payload.locations.map(
+                            (l: any) => l.location,
+                          )}
+                          onSelect={(value) =>
+                            append({ role: 'user', content: value })
+                          }
+                        />
+                      ) : toolName === 'getAppointmentAvailability' ? (
+                        <AvailabilityDisplay
+                          availability={result.payload}
+                          onSelect={(iso) =>
+                            append({ role: 'user', content: iso })
+                          }
+                        />
+                      ) : toolName === 'getUserProfile' ? (
+                        <ProfileCard user={result.payload.user} />
                       ) : (
                         <pre>{JSON.stringify(result, null, 2)}</pre>
                       )}
diff --git a/components/messages.tsx b/components/messages.tsx
index d4e1a02712..0696707f4a 100644
--- a/components/messages.tsx
+++ b/components/messages.tsx
@@ -15,6 +15,7 @@ interface MessagesProps {
   messages: Array<UIMessage>;
   setMessages: UseChatHelpers['setMessages'];
   reload: UseChatHelpers['reload'];
+  append: UseChatHelpers['append'];
   isReadonly: boolean;
   isArtifactVisible: boolean;
 }
@@ -26,6 +27,7 @@ function PureMessages({
   messages,
   setMessages,
   reload,
+  append,
   isReadonly,
 }: MessagesProps) {
   const {
@@ -59,6 +61,7 @@ function PureMessages({
           }
           setMessages={setMessages}
           reload={reload}
+          append={append}
           isReadonly={isReadonly}
           requiresScrollPadding={
             hasSentMessage && index === messages.length - 1
diff --git a/components/option-buttons.tsx b/components/option-buttons.tsx
new file mode 100644
index 0000000000..c8fb76c9ad
--- /dev/null
+++ b/components/option-buttons.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { Button } from './ui/button';
+import { motion } from 'framer-motion';
+
+interface OptionButtonsProps {
+  options: Array<string>;
+  onSelect: (value: string) => void;
+}
+
+export function OptionButtons({ options, onSelect }: OptionButtonsProps) {
+  return (
+    <div className="flex flex-wrap gap-2">
+      {options.map((option, index) => (
+        <motion.div
+          key={option}
+          initial={{ opacity: 0, y: 10 }}
+          animate={{ opacity: 1, y: 0, transition: { delay: 0.05 * index } }}
+        >
+          <Button variant="outline" onClick={() => onSelect(option)}>
+            {option}
+          </Button>
+        </motion.div>
+      ))}
+    </div>
+  );
+}
diff --git a/components/profile-card.tsx b/components/profile-card.tsx
new file mode 100644
index 0000000000..40ff530e81
--- /dev/null
+++ b/components/profile-card.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import Image from 'next/image';
+
+interface UserProfile {
+  name: string;
+  profileImage: string;
+  recentCuts?: Array<string>;
+}
+
+export function ProfileCard({ user }: { user: UserProfile }) {
+  return (
+    <div className="flex flex-col items-center gap-3">
+      <Image
+        src={user.profileImage}
+        alt={`${user.name} profile`}
+        width={80}
+        height={80}
+        className="rounded-full"
+      />
+      <div className="font-medium">{user.name}</div>
+      {user.recentCuts && user.recentCuts.length > 0 && (
+        <div className="flex gap-2">
+          {user.recentCuts.map((url) => (
+            <Image
+              key={url}
+              src={url}
+              alt="Recent cut"
+              width={60}
+              height={60}
+              className="rounded-md"
+            />
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 7c4a8410ab..7a594e1873 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -11,6 +11,6 @@ export default defineConfig({
   dialect: 'postgresql',
   dbCredentials: {
     // biome-ignore lint: Forbidden non-null assertion.
-    url: process.env.POSTGRES_URL!,
+    url: process.env.POSTGRES_URL ?? process.env.DATABASE_URL!,
   },
 });
diff --git a/lib/ai/tools/danckuts.ts b/lib/ai/tools/danckuts.ts
new file mode 100644
index 0000000000..fd6a0c2e1e
--- /dev/null
+++ b/lib/ai/tools/danckuts.ts
@@ -0,0 +1,93 @@
+import { tool } from 'ai';
+import { z } from 'zod';
+import {
+  getUserProfile,
+  getAllLocations,
+  checkPhoneNumberHasUser,
+  sendTextCode,
+  verifyTextCode,
+  getRecentAppointments,
+  cancelAppointment,
+  getAppointmentAvailability,
+  rescheduleAppointment,
+  scheduleAppointment,
+} from '@/lib/api/danckuts';
+
+export const danckutsTools = {
+  getUserProfile: tool({
+    description: 'Retrieve the authenticated user profile',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getUserProfile(token),
+  }),
+  getAllLocations: tool({
+    description: 'Get list of all salon locations',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getAllLocations(token),
+  }),
+  checkPhoneNumberHasUser: tool({
+    description: 'Check if a phone number has an account',
+    parameters: z.object({ phoneNumber: z.string() }),
+    execute: async ({ phoneNumber }) => checkPhoneNumberHasUser(phoneNumber),
+  }),
+  sendTextCode: tool({
+    description: 'Send authentication text code',
+    parameters: z.object({ phoneNumber: z.string() }),
+    execute: async ({ phoneNumber }) => sendTextCode(phoneNumber),
+  }),
+  verifyTextCode: tool({
+    description: 'Verify authentication text code',
+    parameters: z.object({ phoneNumber: z.string(), code: z.string() }),
+    execute: async ({ phoneNumber, code }) => verifyTextCode(phoneNumber, code),
+  }),
+  getRecentAppointments: tool({
+    description: 'Retrieve user appointments',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getRecentAppointments(token),
+  }),
+  cancelAppointment: tool({
+    description: 'Cancel an appointment',
+    parameters: z.object({ token: z.string(), appointmentId: z.number() }),
+    execute: async ({ token, appointmentId }) =>
+      cancelAppointment(token, appointmentId),
+  }),
+  getAppointmentAvailability: tool({
+    description: 'Get appointment availability for location and dates',
+    parameters: z.object({
+      token: z.string(),
+      location: z.string(),
+      dates: z.array(z.string()).optional(),
+    }),
+    execute: async ({ token, location, dates }) =>
+      getAppointmentAvailability(token, location, dates),
+  }),
+  rescheduleAppointment: tool({
+    description: 'Reschedule an appointment',
+    parameters: z.object({
+      token: z.string(),
+      appointmentId: z.string(),
+      location: z.string(),
+      time: z.string(),
+    }),
+    execute: async ({ token, appointmentId, location, time }) =>
+      rescheduleAppointment(token, appointmentId, location, time),
+  }),
+  scheduleAppointment: tool({
+    description: 'Schedule a new appointment',
+    parameters: z.object({
+      token: z.string(),
+      location: z.string(),
+      time: z.string(),
+      dependentId: z.number().optional(),
+      serviceType: z.string().optional(),
+    }),
+    execute: async ({ token, location, time, dependentId, serviceType }) =>
+      scheduleAppointment(token, location, time, dependentId, serviceType),
+  }),
+  showOptions: tool({
+    description: 'Render selectable options to the user',
+    parameters: z.object({
+      options: z.array(z.string()),
+    }),
+    execute: async ({ options }) => ({ options }),
+  }),
+};
diff --git a/lib/api/danckuts.ts b/lib/api/danckuts.ts
new file mode 100644
index 0000000000..667b867929
--- /dev/null
+++ b/lib/api/danckuts.ts
@@ -0,0 +1,103 @@
+const BASE_URL = process.env.DANCKUTS_API_BASE_URL || 'http://atlas.danckuts.com';
+
+async function request(path: string, options: RequestInit = {}, token?: string) {
+  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
+  if (token) {
+    headers['Authorization'] = `Bearer ${token}`;
+  }
+  const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
+  if (!res.ok) {
+    throw new Error(`Danckuts API error: ${res.status}`);
+  }
+  return res.json();
+}
+
+export async function getUserProfile(token: string) {
+  return request('/api/auth/me', { method: 'GET' }, token);
+}
+
+export async function getAllLocations(token: string) {
+  return request('/api/locations/hidden', { method: 'GET' }, token);
+}
+
+export async function checkPhoneNumberHasUser(phoneNumber: string) {
+  return request('/api/auth/phone-number-has-user', {
+    method: 'POST',
+    body: JSON.stringify({ phoneNumber }),
+  });
+}
+
+export async function sendTextCode(phoneNumber: string) {
+  return request('/api/auth/send-text-code', {
+    method: 'POST',
+    body: JSON.stringify({ phoneNumber }),
+  });
+}
+
+export async function verifyTextCode(phoneNumber: string, code: string) {
+  return request('/api/auth/check-text-code', {
+    method: 'POST',
+    body: JSON.stringify({ phoneNumber, code }),
+  });
+}
+
+export async function getRecentAppointments(token: string) {
+  return request('/api/appointments/recent', { method: 'GET' }, token);
+}
+
+export async function cancelAppointment(token: string, appointmentId: number) {
+  return request(
+    '/api/appointments/cancel',
+    {
+      method: 'POST',
+      body: JSON.stringify({ appointmentId }),
+    },
+    token,
+  );
+}
+
+export async function getAppointmentAvailability(
+  token: string,
+  location: string,
+  dates?: string[],
+) {
+  const params = new URLSearchParams();
+  params.set('location', location);
+  if (dates) {
+    for (const d of dates) params.append('dates', d);
+  }
+  return request(`/api/time/availability?${params.toString()}`, { method: 'GET' }, token);
+}
+
+export async function rescheduleAppointment(
+  token: string,
+  appointmentId: string,
+  location: string,
+  time: string,
+) {
+  return request(
+    '/api/appointments/reschedule',
+    {
+      method: 'POST',
+      body: JSON.stringify({ appointmentId, location, time }),
+    },
+    token,
+  );
+}
+
+export async function scheduleAppointment(
+  token: string,
+  location: string,
+  time: string,
+  dependentId?: number,
+  serviceType?: string,
+) {
+  return request(
+    '/api/appointments/schedule',
+    {
+      method: 'POST',
+      body: JSON.stringify({ location, time, dependentId, serviceType }),
+    },
+    token,
+  );
+}
diff --git a/lib/db/helpers/01-core-to-parts.ts b/lib/db/helpers/01-core-to-parts.ts
index 81c2693f35..9e97c7822a 100644
--- a/lib/db/helpers/01-core-to-parts.ts
+++ b/lib/db/helpers/01-core-to-parts.ts
@@ -16,11 +16,13 @@ config({
   path: '.env.local',
 });
 
-if (!process.env.POSTGRES_URL) {
-  throw new Error('POSTGRES_URL environment variable is not set');
+const url = process.env.POSTGRES_URL ?? process.env.DATABASE_URL;
+
+if (!url) {
+  throw new Error('POSTGRES_URL or DATABASE_URL environment variable is not set');
 }
 
-const client = postgres(process.env.POSTGRES_URL);
+const client = postgres(url);
 const db = drizzle(client);
 
 const BATCH_SIZE = 100; // Process 100 chats at a time
diff --git a/lib/db/migrate.ts b/lib/db/migrate.ts
index 1280e75980..2b9f16e4d3 100644
--- a/lib/db/migrate.ts
+++ b/lib/db/migrate.ts
@@ -8,11 +8,13 @@ config({
 });
 
 const runMigrate = async () => {
-  if (!process.env.POSTGRES_URL) {
-    throw new Error('POSTGRES_URL is not defined');
+  const url = process.env.POSTGRES_URL ?? process.env.DATABASE_URL;
+
+  if (!url) {
+    throw new Error('POSTGRES_URL or DATABASE_URL is not defined');
   }
 
-  const connection = postgres(process.env.POSTGRES_URL, { max: 1 });
+  const connection = postgres(url, { max: 1 });
   const db = drizzle(connection);
 
   console.log('⏳ Running migrations...');
diff --git a/lib/db/queries.ts b/lib/db/queries.ts
index 2f8b2b2079..8a9a42f229 100644
--- a/lib/db/queries.ts
+++ b/lib/db/queries.ts
@@ -39,7 +39,7 @@ import { ChatSDKError } from '../errors';
 // https://authjs.dev/reference/adapter/drizzle
 
 // biome-ignore lint: Forbidden non-null assertion.
-const client = postgres(process.env.POSTGRES_URL!);
+const client = postgres(process.env.POSTGRES_URL ?? process.env.DATABASE_URL!);
 const db = drizzle(client);
 
 export async function getUser(email: string): Promise<Array<User>> {


Reviews:
user: login: sourcery-ai[bot]
body: Hey @skalingclouds - I've reviewed your changes - here's some feedback:

- Consider extracting the list of tool names in `route.ts` dynamically from `danckutsTools` keys to avoid manual duplication and potential mismatches.
- The `AvailabilityDisplay` maps formatted time labels back to ISO strings via a search which could collide if labels aren’t unique—bind the ISO directly to each button instead.
- You’re passing both `setMessages` and `append` through multiple components—consider bundling them into a single chat helpers object to reduce prop drilling.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 4 issues found
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-18T03:44:35Z

Review comments:
diff_hunk: @@ -212,6 +217,31 @@ const PurePreviewMessage = ({
                           result={result}
                           isReadonly={isReadonly}
                         />
+                      ) : toolName === 'showOptions' ? (
path: components/message.tsx
user: login: sourcery-ai[bot]
body: **suggestion:** Nested ternary for toolName is hard to maintain

Refactor the `toolName` ternary chain into a `switch` or a map of tool names to components for clearer, more scalable code.

Suggested implementation:

```typescript
}) => {
  // Replace nested ternary with a lookup map
  const toolMap: Record<string, React.ReactNode> = {
    showOptions: (
      <OptionButtons
        options={result.options}
        onSelect={(value) => append({ role: 'user', content: value })}
      />
    ),
    getAllLocations: (
      <OptionButtons
        options={result.payload.locations.map((l: any) => l.location)}
        onSelect={(value) => append({ role: 'user', content: value })}
      />
    ),
  };
  const toolComponent = toolMap[toolName] ?? (
    <ToolComponent result={result} isReadonly={isReadonly} />
  );

  return (
    <>
      {toolComponent}

```

1. Ensure you remove any leftover bits of the old ternary beyond what’s shown here so there’s no dangling JSX.
2. Make sure `ToolComponent` is imported if it isn’t already.
3. Adjust surrounding indentation/`return ( … )` structure as needed to fit your file.
created_at: 2025-05-18T03:44:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,27 @@
+'use client';
+
+import { Button } from './ui/button';
+import { motion } from 'framer-motion';
+
+interface OptionButtonsProps {
+  options: Array<string>;
+  onSelect: (value: string) => void;
+}
+
+export function OptionButtons({ options, onSelect }: OptionButtonsProps) {
+  return (
+    <div className="flex flex-wrap gap-2">
+      {options.map((option, index) => (
+        <motion.div
+          key={option}
path: components/option-buttons.tsx
user: login: sourcery-ai[bot]
body: **nitpick (bug_risk):** Using option string as React key may cause duplicates

If `options` may repeat, `key={option}` can collide. Combine the option with its index or another unique identifier to ensure unique keys.
created_at: 2025-05-18T03:44:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,103 @@
+const BASE_URL = process.env.DANCKUTS_API_BASE_URL || 'http://atlas.danckuts.com';
+
+async function request(path: string, options: RequestInit = {}, token?: string) {
path: lib/api/danckuts.ts
user: login: sourcery-ai[bot]
body: **suggestion:** Unstructured JSON response lacks typing

Make `request` generic (e.g. `request<T>`) or add a schema validator (like zod) so callers get properly typed responses instead of `any`.
created_at: 2025-05-18T03:44:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,93 @@
+import { tool } from 'ai';
+import { z } from 'zod';
+import {
+  getUserProfile,
+  getAllLocations,
+  checkPhoneNumberHasUser,
+  sendTextCode,
+  verifyTextCode,
+  getRecentAppointments,
+  cancelAppointment,
+  getAppointmentAvailability,
+  rescheduleAppointment,
+  scheduleAppointment,
+} from '@/lib/api/danckuts';
+
+export const danckutsTools = {
+  getUserProfile: tool({
+    description: 'Retrieve the authenticated user profile',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getUserProfile(token),
+  }),
+  getAllLocations: tool({
+    description: 'Get list of all salon locations',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getAllLocations(token),
+  }),
+  checkPhoneNumberHasUser: tool({
+    description: 'Check if a phone number has an account',
+    parameters: z.object({ phoneNumber: z.string() }),
+    execute: async ({ phoneNumber }) => checkPhoneNumberHasUser(phoneNumber),
+  }),
+  sendTextCode: tool({
+    description: 'Send authentication text code',
+    parameters: z.object({ phoneNumber: z.string() }),
+    execute: async ({ phoneNumber }) => sendTextCode(phoneNumber),
+  }),
+  verifyTextCode: tool({
+    description: 'Verify authentication text code',
+    parameters: z.object({ phoneNumber: z.string(), code: z.string() }),
+    execute: async ({ phoneNumber, code }) => verifyTextCode(phoneNumber, code),
+  }),
+  getRecentAppointments: tool({
+    description: 'Retrieve user appointments',
+    parameters: z.object({ token: z.string() }),
+    execute: async ({ token }) => getRecentAppointments(token),
+  }),
+  cancelAppointment: tool({
+    description: 'Cancel an appointment',
+    parameters: z.object({ token: z.string(), appointmentId: z.number() }),
+    execute: async ({ token, appointmentId }) =>
+      cancelAppointment(token, appointmentId),
+  }),
+  getAppointmentAvailability: tool({
+    description: 'Get appointment availability for location and dates',
+    parameters: z.object({
+      token: z.string(),
+      location: z.string(),
+      dates: z.array(z.string()).optional(),
+    }),
+    execute: async ({ token, location, dates }) =>
+      getAppointmentAvailability(token, location, dates),
+  }),
+  rescheduleAppointment: tool({
+    description: 'Reschedule an appointment',
path: lib/ai/tools/danckuts.ts
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Inconsistent `appointmentId` type across tools

Standardize appointmentId to a single type across cancelAppointment and rescheduleAppointment to prevent mismatches.
created_at: 2025-05-18T03:44:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,40 @@
+'use client';
+
+import { format } from 'date-fns';
+import { OptionButtons } from './option-buttons';
+
+interface Availability {
+  dates: Record<string, string[]>;
+  slots: Record<string, Record<string, number>>;
+}
+
+export function AvailabilityDisplay({
+  availability,
+  onSelect,
+}: {
+  availability: Availability;
+  onSelect: (iso: string) => void;
+}) {
+  return (
+    <div className="flex flex-col gap-4">
+      {Object.entries(availability.slots).map(([date, times]) => (
+        <div key={date} className="flex flex-col gap-2">
+          <div className="font-medium">
+            {format(new Date(date), 'PPP')}
+          </div>
+          <OptionButtons
+            options={Object.keys(times).map((iso) =>
+              format(new Date(iso), 'p'),
+            )}
+            onSelect={(label) => {
+              const iso = Object.keys(times).find(
+                (t) => format(new Date(t), 'p') === label,
+              );
+              if (iso) onSelect(iso);
path: components/availability.tsx
user: login: sourcery-ai[bot]
body: **suggestion (code-quality):** Use block braces for ifs, whiles, etc. ([`use-braces`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-braces))

```suggestion
              if (iso) {
```

<br/><details><summary>Explanation</summary>It is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).
</details>
created_at: 2025-05-18T03:44:35Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-05-18T03:41:31Z
body: [vc]: #iqWW/XV5UizaryNLmI7OeauxWaIcDjlwxcNWDtuaHRU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhaS1jaGF0Ym90LWVhdDUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbmFpdGl2ZS9haS1jaGF0Ym90LWVhdDUvQm5qY0hYaXNrWVRWaXRwdXhlWVVpcTNoOUpzSiIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiRkFJTEVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **ai-chatbot-eat5** | ❌ Failed ([Inspect](https://vercel.com/naitive/ai-chatbot-eat5/BnjcHXiskYTVitpuxeYUiq3h9JsJ)) |  |  | May 18, 2025 3:41am |


author_association: NONE
user: login: sourcery-ai[bot]
created_at: 2025-05-18T03:41:34Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

Implements fallback to DATABASE_URL for database connections, integrates Danckuts API tools into the chat AI with corresponding UI components for interactive responses, updates the chat API route to include new tools, documents the fallback in README and .env.example, and applies minor dependency fixes in auth pages.

#### Sequence Diagram: AI Tool Execution with Danckuts API

```mermaid
sequenceDiagram
    actor User
    participant ChatUI
    participant BackendChatAPI
    participant AIModel
    participant DanckutsTools
    participant DanckutsAPIClient
    participant ExternalDanckutsAPI

    User->>ChatUI: Sends message (e.g., "Show my profile")
    ChatUI->>BackendChatAPI: POST /api/chat (message)
    BackendChatAPI->>AIModel: Process message with available tools
    AIModel-->>DanckutsTools: Decide to use tool (e.g., getUserProfile)
    DanckutsTools->>DanckutsAPIClient: Calls getUserProfile(token)
    DanckutsAPIClient->>ExternalDanckutsAPI: GET /api/auth/me Request
    ExternalDanckutsAPI-->>DanckutsAPIClient: User profile data Response
    DanckutsAPIClient-->>DanckutsTools: Returns profile data
    DanckutsTools-->>AIModel: Tool execution result (profile data)
    AIModel->>BackendChatAPI: Generates response including profile data
    BackendChatAPI-->>ChatUI: Streams response
    ChatUI->>User: Displays profile using ProfileCard component
```

#### Sequence Diagram: Database Connection with URL Fallback

```mermaid
sequenceDiagram
    participant AppCode as "Application Code<br>(e.g., migrate.ts, queries.ts)"
    participant EnvVars as "Environment Variables"
    participant PGClient as "PostgreSQL Client"

    AppCode->>EnvVars: Attempt to read POSTGRES_URL
    alt POSTGRES_URL is defined
        EnvVars-->>AppCode: Return POSTGRES_URL
        AppCode->>PGClient: Initialize with POSTGRES_URL
    else POSTGRES_URL is not defined
        EnvVars-->>AppCode: null/undefined
        AppCode->>EnvVars: Attempt to read DATABASE_URL
        alt DATABASE_URL is defined
            EnvVars-->>AppCode: Return DATABASE_URL
            AppCode->>PGClient: Initialize with DATABASE_URL
        else DATABASE_URL is not defined
            EnvVars-->>AppCode: null/undefined
            AppCode->>AppCode: Throw Error "POSTGRES_URL or DATABASE_URL not defined"
        end
    end
```

#### Class Diagram: New UI Components

```mermaid
classDiagram
    class AvailabilityDisplay {
      +Availability availability
      +Function onSelect(iso)
      +render()
    }
    class ProfileCard {
      +UserProfile user
      +render()
    }
    class OptionButtons {
      +String[] options
      +Function onSelect(value)
      +render()
    }
    class UserProfile {
      <<Interface>>
      +String name
      +String profileImage
      +String[] recentCuts
    }
    ProfileCard ..> UserProfile : uses
```

#### Class Diagram: Modified Chat UI Components

```mermaid
classDiagram
    class PurePreviewMessage {
      +UseChatHelpers['setMessages'] setMessages
      +UseChatHelpers['reload'] reload
      +UseChatHelpers['append'] append
      +Boolean isLoading
      +Boolean isReadonly
      #renderToolOutput()
    }
    note for PurePreviewMessage "Handles rendering for new tool outputs:
- 'showOptions': OptionButtons
- 'getAllLocations': OptionButtons
- 'getAppointmentAvailability': AvailabilityDisplay
- 'getUserProfile': ProfileCard"
    class PureMessages {
      +UIMessage[] messages
      +UseChatHelpers['setMessages'] setMessages
      +UseChatHelpers['reload'] reload
      +UseChatHelpers['append'] append
      +Boolean isReadonly
    }
    class Chat {
      +UseChatHelpers['append'] append
      +displayMessages()
    }
    Chat o--> PureMessages : aggregates
    PureMessages o--> PurePreviewMessage : aggregates
```

#### Class Diagram: Danckuts API Client Module

```mermaid
classDiagram
    class DanckutsAPIClient {
      <<Module: lib/api/danckuts.ts>>
      -String BASE_URL
      -async request(path, options, token)
      +async getUserProfile(token)
      +async getAllLocations(token)
      +async checkPhoneNumberHasUser(phoneNumber)
      +async sendTextCode(phoneNumber)
      +async verifyTextCode(phoneNumber, code)
      +async getRecentAppointments(token)
      +async cancelAppointment(token, appointmentId)
      +async getAppointmentAvailability(token, location, dates)
      +async rescheduleAppointment(token, appointmentId, location, time)
      +async scheduleAppointment(token, location, time, dependentId, serviceType)
    }
```

#### Class Diagram: Danckuts AI Tools Object

```mermaid
classDiagram
    class AI_Tool {
        <<Interface>>
        description: string
        parameters: ZodSchema
        execute(params): Promise~any~
    }
    class DanckutsAITools {
        <<Object: danckutsTools in lib/ai/tools/danckuts.ts>>
        +AI_Tool getUserProfile
        +AI_Tool getAllLocations
        +AI_Tool checkPhoneNumberHasUser
        +AI_Tool sendTextCode
        +AI_Tool verifyTextCode
        +AI_Tool getRecentAppointments
        +AI_Tool cancelAppointment
        +AI_Tool getAppointmentAvailability
        +AI_Tool rescheduleAppointment
        +AI_Tool scheduleAppointment
        +AI_Tool showOptions
    }
    note for DanckutsAITools "Each tool is defined using 'tool()' from the 'ai' library and uses functions from DanckutsAPIClient for execution.
Example tool parameters:
- getUserProfile: { token: string }
- showOptions: { options: string[] }"
    DanckutsAITools ..> DanckutsAPIClient : uses functions from
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implement DATABASE_URL fallback in DB setup and helpers | <ul><li>Use process.env.POSTGRES_URL ?? process.env.DATABASE_URL for connection URL</li><li>Throw error if neither POSTGRES_URL nor DATABASE_URL is set</li><li>Update client and migration connection instantiation</li></ul> | `lib/db/helpers/01-core-to-parts.ts`<br/>`lib/db/migrate.ts`<br/>`drizzle.config.ts`<br/>`lib/db/queries.ts` |
| Document database URL fallback | <ul><li>Add note about DATABASE_URL fallback in README</li><li>Reflect fallback option in .env.example</li></ul> | `README.md`<br/>`.env.example` |
| Add Danckuts API wrappers and tool definitions | <ul><li>Create lib/api/danckuts.ts with HTTP request helpers and endpoints</li><li>Define corresponding AI tools in lib/ai/tools/danckuts.ts</li></ul> | `lib/api/danckuts.ts`<br/>`lib/ai/tools/danckuts.ts` |
| Extend chat API route to include new Danckuts tools | <ul><li>Add new tool names to allowed tools list</li><li>Spread danckutsTools into route tool registry</li></ul> | `app/(chat)/api/chat/route.ts` |
| Enhance chat UI to support interactive tool responses | <ul><li>Propagate new append helper through Chat, Messages, and Message components</li><li>Add conditional rendering for showOptions, getAllLocations, getAppointmentAvailability, and getUserProfile</li></ul> | `components/message.tsx`<br/>`components/messages.tsx`<br/>`components/chat.tsx` |
| Introduce interactive UI components for tool outputs | <ul><li>Implement OptionButtons for selectable options</li><li>Add AvailabilityDisplay to render time slots</li><li>Add ProfileCard for user profile display</li></ul> | `components/option-buttons.tsx`<br/>`components/availability.tsx`<br/>`components/profile-card.tsx` |
| Fix missing dependencies in auth page effects | <ul><li>Include router and updateSession in useEffect dependencies of login page</li><li>Include router and updateSession in useEffect dependencies of register page</li></ul> | `app/(auth)/login/page.tsx`<br/>`app/(auth)/register/page.tsx` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE