suna/frontend/src/app/(dashboard)/dashboard/page.tsx

225 lines
8.2 KiB
TypeScript
Raw Normal View History

2025-04-16 04:45:46 +08:00
"use client";
2025-04-23 12:45:43 +08:00
import React, { useState, Suspense, useEffect, useRef } from 'react';
2025-04-16 04:45:46 +08:00
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from 'next/navigation';
2025-04-21 22:59:20 +08:00
import { Menu } from "lucide-react";
2025-04-23 12:45:43 +08:00
import { ChatInput, ChatInputHandles } from '@/components/thread/chat-input';
import { initiateAgent, createThread, addUserMessage, startAgent, createProject, BillingError } from "@/lib/api";
2025-04-24 20:36:56 +08:00
import { generateThreadName } from "@/lib/actions/threads";
2025-04-21 22:59:20 +08:00
import { useIsMobile } from "@/hooks/use-mobile";
import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
2025-04-21 23:36:40 +08:00
import { useBillingError } from "@/hooks/useBillingError";
2025-04-24 12:30:08 +08:00
import { BillingErrorAlert } from "@/components/billing/usage-limit-alert";
2025-04-21 23:36:40 +08:00
import { useAccounts } from "@/hooks/use-accounts";
2025-04-27 10:20:49 +08:00
import { isLocalMode, config } from "@/lib/config";
2025-04-24 12:30:08 +08:00
import { toast } from "sonner";
2025-04-16 04:45:46 +08:00
// Constant for localStorage key to ensure consistency
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
2025-04-16 04:45:46 +08:00
function DashboardContent() {
const [inputValue, setInputValue] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [autoSubmit, setAutoSubmit] = useState(false);
2025-04-21 23:36:40 +08:00
const { billingError, handleBillingError, clearBillingError } = useBillingError();
2025-04-16 04:45:46 +08:00
const router = useRouter();
2025-04-21 22:59:20 +08:00
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar();
2025-04-21 23:36:40 +08:00
const { data: accounts } = useAccounts();
const personalAccount = accounts?.find(account => account.personal_account);
2025-04-23 12:45:43 +08:00
const chatInputRef = useRef<ChatInputHandles>(null);
2025-04-16 04:45:46 +08:00
2025-04-18 13:42:57 +08:00
const handleSubmit = async (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => {
2025-04-24 20:36:56 +08:00
if ((!message.trim() && !(chatInputRef.current?.getPendingFiles().length)) || isSubmitting) return;
2025-04-16 04:45:46 +08:00
setIsSubmitting(true);
2025-04-24 20:36:56 +08:00
2025-04-16 04:45:46 +08:00
try {
2025-04-24 12:30:08 +08:00
const files = chatInputRef.current?.getPendingFiles() || [];
2025-04-23 12:45:43 +08:00
localStorage.removeItem(PENDING_PROMPT_KEY);
2025-04-24 20:36:56 +08:00
2025-04-24 12:30:08 +08:00
if (files.length > 0) {
// Create a FormData instance
const formData = new FormData();
2025-04-24 20:36:56 +08:00
2025-04-24 12:30:08 +08:00
// Append the message
formData.append('message', message);
2025-04-24 20:36:56 +08:00
2025-04-24 12:30:08 +08:00
// Append all files
files.forEach(file => {
formData.append('files', file);
});
2025-04-24 20:36:56 +08:00
2025-04-24 12:30:08 +08:00
// Add any additional options
if (options) {
formData.append('options', JSON.stringify(options));
}
2025-04-24 20:36:56 +08:00
2025-04-24 12:30:08 +08:00
// Call initiateAgent API
const result = await initiateAgent(formData);
2025-04-24 20:36:56 +08:00
console.log('Agent initiated with files:', result);
2025-04-24 12:30:08 +08:00
// Navigate to the thread
if (result.thread_id) {
router.push(`/agents/${result.thread_id}`);
}
} else {
2025-04-24 20:36:56 +08:00
// ---- Text-only messages ----
// 1. Generate a project name
const projectName = await generateThreadName(message);
// 2. Create the project
// Assuming createProject gets the account_id from the logged-in user
const newProject = await createProject({
name: projectName,
description: "", // Or derive a description if desired
});
// 3. Create the thread using the new project ID
const thread = await createThread(newProject.id); // <-- Pass the actual project ID
// 4. Then add the user message
2025-04-24 12:30:08 +08:00
await addUserMessage(thread.thread_id, message);
2025-04-24 20:36:56 +08:00
// 5. Start the agent on this thread with the options
2025-04-24 12:30:08 +08:00
await startAgent(thread.thread_id, options);
2025-04-24 20:36:56 +08:00
// 6. Navigate to thread
2025-04-24 12:30:08 +08:00
router.push(`/agents/${thread.thread_id}`);
}
} catch (error: any) {
console.error('Error during submission process:', error);
// Check specifically for BillingError (402)
if (error instanceof BillingError) {
console.log("Handling BillingError:", error.detail);
handleBillingError({
// Pass details from the BillingError instance
message: error.detail.message || 'Monthly usage limit reached. Please upgrade your plan.',
currentUsage: error.detail.currentUsage as number | undefined, // Attempt to get usage/limit if backend adds them
limit: error.detail.limit as number | undefined,
// Include subscription details if available in the error, otherwise provide defaults
subscription: error.detail.subscription || {
2025-04-27 10:20:49 +08:00
price_id: config.SUBSCRIPTION_TIERS.FREE.priceId, // Default to Free tier
plan_name: "Free"
2025-04-21 23:36:40 +08:00
}
});
// Don't show toast for billing errors, the modal handles it
setIsSubmitting(false);
return; // Stop execution
2025-04-21 23:36:40 +08:00
}
// Handle other types of errors (e.g., network, other API errors)
// Skip toast in local mode unless it's a connection error
const isConnectionError = error instanceof TypeError && error.message.includes('Failed to fetch');
if (!isLocalMode() || isConnectionError) {
toast.error(error.message || "An unexpected error occurred");
}
setIsSubmitting(false); // Reset submitting state on other errors too
2025-04-16 04:45:46 +08:00
}
// No finally block needed, state is reset in catch blocks
2025-04-16 04:45:46 +08:00
};
2025-04-20 12:29:55 +08:00
// Check for pending prompt in localStorage on mount
useEffect(() => {
// Use a small delay to ensure we're fully mounted
const timer = setTimeout(() => {
const pendingPrompt = localStorage.getItem(PENDING_PROMPT_KEY);
if (pendingPrompt) {
setInputValue(pendingPrompt);
setAutoSubmit(true); // Flag to auto-submit after mounting
}
}, 200);
return () => clearTimeout(timer);
}, []);
// Auto-submit the form if we have a pending prompt
useEffect(() => {
if (autoSubmit && inputValue && !isSubmitting) {
const timer = setTimeout(() => {
handleSubmit(inputValue);
setAutoSubmit(false);
}, 500);
return () => clearTimeout(timer);
}
2025-04-23 12:45:43 +08:00
}, [autoSubmit, inputValue, isSubmitting]);
2025-04-20 12:29:55 +08:00
2025-04-16 01:20:15 +08:00
return (
2025-04-16 04:45:46 +08:00
<div className="flex flex-col items-center justify-center h-full w-full">
2025-04-21 22:59:20 +08:00
{isMobile && (
<div className="absolute top-4 left-4 z-10">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => setOpenMobile(true)}
>
<Menu className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</TooltipTrigger>
<TooltipContent>Open menu</TooltipContent>
</Tooltip>
</div>
)}
2025-04-16 04:45:46 +08:00
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="text-center mb-10">
2025-04-16 08:04:04 +08:00
<h1 className="text-4xl font-medium text-foreground mb-2">Hey </h1>
<h2 className="text-2xl text-muted-foreground">What would you like Suna to do today?</h2>
2025-04-16 04:45:46 +08:00
</div>
<ChatInput
2025-04-23 12:45:43 +08:00
ref={chatInputRef}
2025-04-16 04:45:46 +08:00
onSubmit={handleSubmit}
loading={isSubmitting}
placeholder="Describe what you need help with..."
value={inputValue}
onChange={setInputValue}
2025-04-22 01:16:24 +08:00
hideAttachments={false}
2025-04-16 04:45:46 +08:00
/>
2025-04-16 01:20:15 +08:00
</div>
2025-04-21 23:36:40 +08:00
{/* Billing Error Alert */}
<BillingErrorAlert
message={billingError?.message}
currentUsage={billingError?.currentUsage}
limit={billingError?.limit}
2025-04-21 23:36:40 +08:00
accountId={personalAccount?.account_id}
onDismiss={clearBillingError}
isOpen={!!billingError}
/>
2025-04-16 04:45:46 +08:00
</div>
);
}
export default function DashboardPage() {
return (
<Suspense fallback={
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="flex flex-col items-center text-center mb-10">
<Skeleton className="h-10 w-40 mb-2" />
<Skeleton className="h-7 w-56" />
</div>
<Skeleton className="w-full h-[100px] rounded-xl" />
<div className="flex justify-center mt-3">
<Skeleton className="h-5 w-16" />
</div>
</div>
</div>
}>
<DashboardContent />
</Suspense>
);
2025-04-16 01:20:15 +08:00
}