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
39 changes: 31 additions & 8 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
saveMessage,
updateChat,
getMessagesByChatId,
getUserCustomization,
} from "@/lib/db/actions";
import { v4 as uuidv4 } from "uuid";
import { processChatMessages } from "@/lib/chat/chat-processor";
Expand Down Expand Up @@ -56,20 +57,29 @@ export async function POST(req: NextRequest) {
const { userId, isPro } = await getUserIDAndPro(req);
const userLocation = geolocation(req);

// Check if free user is trying to use agent mode
if (mode === "agent" && !isPro) {
throw new ChatSDKError(
"forbidden:chat",
"Agent mode is only available for Pro users. Please upgrade to access this feature.",
);
}

// Get existing messages, merge with new messages, and truncate
const truncatedMessages = await getMessagesByChatId({
const { truncatedMessages, chat, isNewChat } = await getMessagesByChatId({
chatId,
userId,
newMessages: messages,
regenerate,
});

// Handle initial chat setup, regeneration, and save user message
const { isNewChat } = await handleInitialChatAndUserMessage({
await handleInitialChatAndUserMessage({
chatId,
userId,
messages: truncatedMessages,
regenerate,
chat,
});

// Check rate limit for the user
Expand All @@ -85,6 +95,9 @@ export async function POST(req: NextRequest) {
posthog,
});

// Get user customization data
const userCustomization = await getUserCustomization({ userId });

const stream = createUIMessageStream({
execute: async ({ writer }) => {
const { tools, getSandbox, getTodoManager } = createTools(
Expand All @@ -106,19 +119,29 @@ export async function POST(req: NextRequest) {
: Promise.resolve(undefined);

// Select the appropriate model based on whether media files are present
const selectedModel = hasMediaFiles ? "vision-model" : "agent-model";
let selectedModel = "";
if (mode === "ask") {
selectedModel = "ask-model";
} else {
selectedModel = "agent-model";
}
selectedModel = hasMediaFiles ? "vision-model" : selectedModel;

const result = streamText({
model: myProvider.languageModel(selectedModel),
system: systemPrompt(mode, executionMode),
system: systemPrompt(mode, executionMode, userCustomization),
messages: convertToModelMessages(processedMessages),
...(!isPro && {
providerOptions: {
providerOptions: {
openrouter: {
provider: {
sort: "price",
...(!isPro
? {
sort: "price",
}
: { sort: "latency" }),
},
},
}),
},
tools,
abortSignal: controller.signal,
headers: getAIHeaders(),
Expand Down
4 changes: 2 additions & 2 deletions app/c/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Page(props: { params: Promise<{ id: string }> }) {
return (
<>
<AuthLoading>
<div className="h-screen bg-background flex flex-col overflow-hidden">
<div className="h-full bg-background flex flex-col overflow-hidden">
<div className="flex-1 flex items-center justify-center">
<Loading />
</div>
Expand All @@ -24,7 +24,7 @@ export default function Page(props: { params: Promise<{ id: string }> }) {
</Authenticated>

<Unauthenticated>
<div className="h-screen bg-background flex flex-col overflow-hidden">
<div className="h-full bg-background flex flex-col overflow-hidden">
<div className="flex-1 flex items-center justify-center">
<Loading />
</div>
Expand Down
61 changes: 56 additions & 5 deletions app/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { useAuth } from "@workos-inc/authkit-nextjs/components";
import { PanelLeft, Sparkle, Loader2 } from "lucide-react";
import { PanelLeft, Sparkle, Loader2, SquarePen } from "lucide-react";
import { useGlobalState } from "../contexts/GlobalState";
import { useUpgrade } from "../hooks/useUpgrade";
import { useRouter } from "next/navigation";
import { useIsMobile } from "@/hooks/use-mobile";

interface ChatHeaderProps {
hasMessages: boolean;
Expand All @@ -14,6 +16,8 @@ interface ChatHeaderProps {
id?: string;
chatData?: { title?: string } | null | undefined;
chatSidebarOpen?: boolean;
isExistingChat?: boolean;
isChatNotFound?: boolean;
}

const ChatHeader: React.FC<ChatHeaderProps> = ({
Expand All @@ -23,14 +27,28 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
id,
chatData,
chatSidebarOpen = false,
isExistingChat = false,
isChatNotFound = false,
}) => {
const { user, loading } = useAuth();
const { toggleChatSidebar, hasProPlan, isCheckingProPlan } = useGlobalState();
const {
toggleChatSidebar,
hasProPlan,
isCheckingProPlan,
initializeNewChat,
closeSidebar,
setChatSidebarOpen,
} = useGlobalState();
const { upgradeLoading, handleUpgrade } = useUpgrade();
const router = useRouter();
const isMobile = useIsMobile();

// Show sidebar toggle for logged-in users
const showSidebarToggle = user && !loading;

// Check if we're currently in a chat (use isExistingChat prop for accurate state)
const isInChat = isExistingChat;

const handleSignIn = () => {
window.location.href = "/login";
};
Expand All @@ -39,6 +57,22 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
window.location.href = "/signup";
};

const handleNewChat = () => {
// Close computer sidebar when creating new chat
closeSidebar();

// Close chat sidebar when creating new chat on mobile screens
if (isMobile) {
setChatSidebarOpen(false);
}

// Initialize new chat state using global state function
initializeNewChat();

// Navigate to homepage - Chat component will respond to global state changes
router.push("/");
};

// Show empty state header when no messages and no active chat
if (!hasMessages && !hasActiveChat) {
return (
Expand Down Expand Up @@ -188,13 +222,30 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
<div className="w-full flex flex-row items-center justify-between flex-1 min-w-0 gap-[24px]">
<div className="flex flex-row items-center gap-[6px] flex-1 min-w-0 text-foreground text-lg font-medium">
<span className="whitespace-nowrap text-ellipsis overflow-hidden">
{chatTitle ||
(id && chatData === undefined ? "" : "New Chat")}
{isChatNotFound
? ""
: chatTitle ||
(isExistingChat && chatData === undefined
? ""
: "New Chat")}
</span>
</div>
</div>
</div>
<div className="flex-1"></div>
<div className="flex-1 flex justify-end">
{/* New Chat Button - Only show on mobile when in a chat */}
{isMobile && isInChat && showSidebarToggle && (
<Button
variant="ghost"
size="icon"
aria-label="Start new chat"
onClick={handleNewChat}
className="h-7 w-7"
>
<SquarePen className="size-5" />
</Button>
)}
</div>
</div>
</div>
);
Expand Down
115 changes: 99 additions & 16 deletions app/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Expand All @@ -17,13 +24,14 @@ import {
import { useHotkeys } from "react-hotkeys-hook";
import TextareaAutosize from "react-textarea-autosize";
import { useGlobalState } from "../contexts/GlobalState";
import { useUpgrade } from "../hooks/useUpgrade";
import { TodoPanel } from "./TodoPanel";
import type { ChatStatus } from "@/types";
import { FileUploadPreview } from "./FileUploadPreview";
import { ScrollToBottomButton } from "./ScrollToBottomButton";
import { AttachmentButton } from "./AttachmentButton";
import { useFileUpload } from "../hooks/useFileUpload";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { countInputTokens, MAX_TOKENS } from "@/lib/token-utils";
import { toast } from "sonner";

Expand All @@ -46,18 +54,49 @@ export const ChatInput = ({
isAtBottom = true,
onScrollToBottom,
}: ChatInputProps) => {
const { input, setInput, mode, setMode, uploadedFiles, isUploadingFiles } =
useGlobalState();
const {
input,
setInput,
mode,
setMode,
uploadedFiles,
isUploadingFiles,
hasProPlan,
isCheckingProPlan,
} = useGlobalState();
const {
fileInputRef,
handleFileUploadEvent,
handleRemoveFile,
handleAttachClick,
handlePasteEvent,
} = useFileUpload();
const { handleUpgrade, upgradeLoading } = useUpgrade();
const [agentUpgradeDialogOpen, setAgentUpgradeDialogOpen] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isGenerating = status === "submitted" || status === "streaming";

// Fallback to 'ask' mode if user doesn't have pro plan and somehow has agent mode selected
useEffect(() => {
if (!isCheckingProPlan && !hasProPlan && mode === "agent") {
setMode("ask");
}
}, [hasProPlan, isCheckingProPlan, mode, setMode]);

const handleAgentModeClick = () => {
if (hasProPlan) {
setMode("agent");
} else {
setAgentUpgradeDialogOpen(true);
}
};

const handleUpgradeClick = async () => {
await handleUpgrade();
// Don't close popover here - let it stay open to show loading state
// The popover will naturally close when user navigates to Stripe
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Allow submission if there's text input or files attached, and no files are uploading
Expand Down Expand Up @@ -213,20 +252,40 @@ export const ChatInput = ({
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setMode("agent")}
className="cursor-pointer"
>
<Infinity className="w-4 h-4 mr-2" />
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="font-medium">Agent</span>
{hasProPlan || isCheckingProPlan ? (
<DropdownMenuItem
onClick={() => setMode("agent")}
className="cursor-pointer"
>
<Infinity className="w-4 h-4 mr-2" />
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="font-medium">Agent</span>
</div>
<span className="text-xs text-muted-foreground">
Hack, test, secure anything
</span>
</div>
<span className="text-xs text-muted-foreground">
Hack, test, secure anything
</span>
</div>
</DropdownMenuItem>
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={handleAgentModeClick}
className="cursor-pointer"
>
<Infinity className="w-4 h-4 mr-2" />
<div className="flex flex-col flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">Agent</span>
<span className="flex items-center gap-1 rounded-full py-1 px-2 text-xs font-medium bg-[#F1F1FB] text-[#5D5BD0] hover:bg-[#E4E4F6] dark:bg-[#373669] dark:text-[#DCDBF6] dark:hover:bg-[#414071] border-0 transition-all duration-200">
PRO
</span>
</div>
<span className="text-xs text-muted-foreground">
Hack, test, secure anything
</span>
</div>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down Expand Up @@ -291,6 +350,30 @@ export const ChatInput = ({
</div>
)}
</div>

{/* Agent Upgrade Dialog */}
<Dialog
open={agentUpgradeDialogOpen}
onOpenChange={setAgentUpgradeDialogOpen}
>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Upgrade to Pro</DialogTitle>
<DialogDescription>
Get access to Agent mode and unlock advanced hacking, testing, and
security features with Pro.
</DialogDescription>
</DialogHeader>
<Button
onClick={handleUpgradeClick}
disabled={upgradeLoading}
className="w-full"
size="lg"
>
{upgradeLoading ? "Loading..." : "Upgrade to Pro"}
</Button>
</DialogContent>
</Dialog>
</div>
);
};
Loading