Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export async function POST(req: NextRequest) {
model: myProvider.languageModel(selectedModel),
system: systemPrompt(mode, executionMode),
messages: convertToModelMessages(processedMessages),
...(!isPro && {
providerOptions: {
provider: {
sort: "price",
},
},
}),
Comment on lines +115 to +121
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Fix TS spread of boolean union; use a ternary to avoid “Spread types may only be created from object types”.

...(!isPro && {...}) can resolve to false | {…}, which TypeScript rejects in object spread. Switch to a ternary; also mark the literal as const.

Apply this diff:

-          ...(!isPro && {
-            providerOptions: {
-              provider: {
-                sort: "price",
-              },
-            },
-          }),
+          ...(isPro
+            ? {}
+            : {
+                providerOptions: {
+                  provider: {
+                    sort: "price" as const,
+                  },
+                },
+              }),

Run to confirm there are no other boolean-union object spreads:


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts --type=tsx '\.\.\s*\((?:!|\w+\s*[=!]==\s*false)[^)]+\)'

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Search for boolean-union object spreads across all TS and TSX files
rg -nP -g '*.ts' -g '*.tsx' '\.\.\s*\((?:!|\w+\s*[=!]==\s*false)[^)]+\)'

Length of output: 74


🏁 Script executed:

#!/bin/bash
# Search for any spread of a parenthesized expression across TS and TSX files
rg -nP -g '*.ts' -g '*.tsx' '\.\.\.\s*\('

Length of output: 327


Fix boolean-union spreads across TS files; use ternary and as const
TypeScript rejects spreading false | object. Replace all ...(cond && {…}) patterns with ternaries that return {} when false, and mark literals as const. For example:

- ...(country && { userLocation: country }),
+ ...(country ? { userLocation: country } : {}),

- ...(process.env.EXA_API_KEY && { apiKey: process.env.EXA_API_KEY }),
+ ...(process.env.EXA_API_KEY
+    ? { apiKey: process.env.EXA_API_KEY }
+    : {}),

- ...(process.env.EXA_API_KEY && { web: allTools.web }),
+ ...(process.env.EXA_API_KEY
+    ? { web: allTools.web }
+    : {}),

- ...(!isPro && {
-   providerOptions: {
-     provider: { sort: "price" },
-   },
- }),
+ ...(isPro
+    ? {}
+    : {
+        providerOptions: {
+          provider: { sort: "price" as const },
+        },
+      }),

Applies to:

  • lib/ai/tools/web.ts :77
  • lib/ai/tools/index.ts :54, 65
  • app/api/chat/route.ts :115–121
📝 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.

Suggested change
...(!isPro && {
providerOptions: {
provider: {
sort: "price",
},
},
}),
...(isPro
? {}
: {
providerOptions: {
provider: { sort: "price" as const },
},
}),
🤖 Prompt for AI Agents
In app/api/chat/route.ts around lines 115 to 121 (and also update
lib/ai/tools/web.ts:77 and lib/ai/tools/index.ts:54,65), replace the
boolean-union spread pattern `...(cond && { ... })` with a ternary that yields
the object when true and an empty object when false (e.g. `...(cond ? { ... } :
{})`), and mark any literal provider configuration objects with `as const` to
preserve literal types; ensure each replaced spread returns a plain object so
TypeScript no longer sees `false | object`.

tools,
abortSignal: controller.signal,
headers: getAIHeaders(),
Expand Down
2 changes: 1 addition & 1 deletion app/c/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function Page(props: { params: Promise<{ id: string }> }) {
</AuthLoading>

<Authenticated>
<Chat key={chatId} id={chatId} />
<Chat chatId={chatId} />
</Authenticated>

<Unauthenticated>
Expand Down
30 changes: 18 additions & 12 deletions app/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,15 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
<div className="py-3 flex items-center justify-between md:hidden">
<div className="flex items-center gap-2">
{showSidebarToggle && !chatSidebarOpen && (
<div className="flex h-7 w-7 items-center justify-center cursor-pointer rounded-md hover:bg-muted/50 mr-2">
<PanelLeft
className="size-5 text-muted-foreground cursor-pointer"
onClick={toggleChatSidebar}
/>
</div>
<Button
variant="ghost"
size="icon"
aria-label="Toggle chat sidebar"
onClick={toggleChatSidebar}
className="h-7 w-7 mr-2"
>
<PanelLeft className="size-5" />
</Button>
)}
{/* Show upgrade button for logged-in users without pro plan */}
{!loading && user && !isCheckingProPlan && !hasProPlan && (
Expand Down Expand Up @@ -169,12 +172,15 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
<div className="relative flex items-center">
{/* Only show sidebar toggle on mobile - desktop uses collapsed sidebar logo */}
{showSidebarToggle && !chatSidebarOpen && (
<div className="flex h-7 w-7 items-center justify-center cursor-pointer rounded-md hover:bg-muted/50 md:hidden">
<PanelLeft
className="size-5 text-muted-foreground cursor-pointer"
onClick={toggleChatSidebar}
/>
</div>
<Button
variant="ghost"
size="icon"
aria-label="Open sidebar"
onClick={toggleChatSidebar}
className="h-7 w-7 md:hidden"
>
<PanelLeft className="size-5" />
</Button>
)}
</div>
</div>
Expand Down
139 changes: 126 additions & 13 deletions app/components/ChatItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"use client";

import React, { useState } from "react";
import React, { useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { useGlobalState } from "../contexts/GlobalState";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Ellipsis, Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Ellipsis, Trash2, Edit2 } from "lucide-react";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";

Expand All @@ -25,28 +34,33 @@ const ChatItem: React.FC<ChatItemProps> = ({ id, title, isActive = false }) => {
const router = useRouter();
const [isHovered, setIsHovered] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const {
closeSidebar,
setChatSidebarOpen,
currentChatId,
initializeChat,
initializeNewChat,
} = useGlobalState();
const [showRenameDialog, setShowRenameDialog] = useState(false);
const [editTitle, setEditTitle] = useState(title);
const [isRenaming, setIsRenaming] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

const { closeSidebar, setChatSidebarOpen, initializeNewChat } =
useGlobalState();
const isMobile = useIsMobile();
const deleteChat = useMutation(api.chats.deleteChat);
const renameChat = useMutation(api.chats.renameChat);

// Use global currentChatId to determine if this item is active
const isCurrentlyActive = currentChatId === id;
// Check if this chat is currently active based on URL
const isCurrentlyActive = window.location.pathname === `/c/${id}`;

const handleClick = () => {
// Don't navigate if dialog is open or dropdown is open
if (showRenameDialog || isDropdownOpen) {
return;
}

closeSidebar();

if (isMobile) {
setChatSidebarOpen(false);
}

// Use the new initializeChat function for consistent state management
initializeChat(id, true);
// Navigate to the chat route
router.push(`/c/${id}`);
};

Expand All @@ -67,7 +81,60 @@ const ChatItem: React.FC<ChatItemProps> = ({ id, title, isActive = false }) => {
}
};

const handleRename = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Close dropdown first, then open dialog with a small delay to avoid focus conflicts
setIsDropdownOpen(false);
setEditTitle(title); // Set the current title when opening dialog

// Small delay to ensure dropdown is fully closed before opening dialog
setTimeout(() => {
setShowRenameDialog(true);
}, 50);
};

const handleSaveRename = async () => {
const trimmedTitle = editTitle.trim();

// Don't save if title is empty or unchanged
if (!trimmedTitle || trimmedTitle === title) {
setShowRenameDialog(false);
setEditTitle(title); // Reset to original title
return;
}

try {
setIsRenaming(true);
await renameChat({ chatId: id, newTitle: trimmedTitle });
setShowRenameDialog(false);
} catch (error) {
console.error("Failed to rename chat:", error);
setEditTitle(title); // Reset to original title on error
} finally {
setIsRenaming(false);
}
};

const handleCancelRename = () => {
setShowRenameDialog(false);
setEditTitle(title); // Reset to original title
};

const handleInputKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleSaveRename();
} else if (e.key === "Escape") {
e.preventDefault();
handleCancelRename();
}
};

const handleKeyDown = (e: React.KeyboardEvent) => {
// Don't handle keyboard events if dialog or dropdown is open
if (showRenameDialog || isDropdownOpen) return;

if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
Expand Down Expand Up @@ -130,6 +197,10 @@ const ChatItem: React.FC<ChatItemProps> = ({ id, title, isActive = false }) => {
className="z-50 py-2"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem onClick={handleRename}>
<Edit2 className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
Expand All @@ -140,6 +211,48 @@ const ChatItem: React.FC<ChatItemProps> = ({ id, title, isActive = false }) => {
</DropdownMenuContent>
</DropdownMenu>
</div>

{/* Rename Dialog */}
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Rename Chat</DialogTitle>
<DialogDescription>
Enter a new name for this chat conversation.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
ref={inputRef}
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={handleInputKeyDown}
disabled={isRenaming}
placeholder="Chat name"
maxLength={100}
className="w-full"
autoFocus
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={handleCancelRename}
disabled={isRenaming}
>
Cancel
</Button>
<Button
type="button"
onClick={handleSaveRename}
disabled={isRenaming || !editTitle.trim()}
>
{isRenaming ? "Saving..." : "Save"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
Expand Down
Loading