mirror of https://github.com/kortix-ai/suna.git
409 lines
14 KiB
TypeScript
409 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, Suspense, useCallback, useEffect } from 'react';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import Joyride, { CallBackProps, STATUS, Step } from 'react-joyride';
|
|
import {
|
|
ChatInput,
|
|
ChatInputHandles,
|
|
} from '@/components/thread/chat-input/chat-input';
|
|
import {
|
|
BillingError,
|
|
AgentRunLimitError,
|
|
} from '@/lib/api';
|
|
import { useIsMobile } from '@/hooks/use-mobile';
|
|
import { useBillingError } from '@/hooks/useBillingError';
|
|
import { BillingErrorAlert } from '@/components/billing/usage-limit-alert';
|
|
import { useAccounts } from '@/hooks/use-accounts';
|
|
import { config, isLocalMode, isStagingMode } from '@/lib/config';
|
|
import { useInitiateAgentWithInvalidation } from '@/hooks/react-query/dashboard/use-initiate-agent';
|
|
|
|
import { useAgents } from '@/hooks/react-query/agents/use-agents';
|
|
import { cn } from '@/lib/utils';
|
|
import { BillingModal } from '@/components/billing/billing-modal';
|
|
import { useAgentSelection } from '@/lib/stores/agent-selection-store';
|
|
import { Examples } from './examples';
|
|
import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
|
|
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
|
import { KortixLogo } from '../sidebar/kortix-logo';
|
|
import { AgentRunLimitDialog } from '@/components/thread/agent-run-limit-dialog';
|
|
import { useFeatureFlag } from '@/lib/feature-flags';
|
|
import { CustomAgentsSection } from './custom-agents-section';
|
|
import { toast } from 'sonner';
|
|
import { ReleaseBadge } from '../auth/release-badge';
|
|
import { useDashboardTour } from '@/hooks/use-dashboard-tour';
|
|
import { TourConfirmationDialog } from '@/components/tour/TourConfirmationDialog';
|
|
import { Calendar, MessageSquare, Plus, Sparkles, Zap } from 'lucide-react';
|
|
|
|
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
|
|
|
const dashboardTourSteps: Step[] = [
|
|
{
|
|
target: '[data-tour="chat-input"]',
|
|
content: 'Type your questions or tasks here. Suna can help with research, analysis, automation, and much more.',
|
|
title: 'Start a Conversation',
|
|
placement: 'top',
|
|
disableBeacon: true,
|
|
},
|
|
{
|
|
target: '[data-tour="my-agents"]',
|
|
content: 'Create and manage your custom AI agents here. Build specialized agents for different tasks and workflows.',
|
|
title: 'Manage Your Agents',
|
|
placement: 'right',
|
|
disableBeacon: true,
|
|
},
|
|
{
|
|
target: '[data-tour="examples"]',
|
|
content: 'Get started quickly with these example prompts. Click any example to try it out.',
|
|
title: 'Example Prompts',
|
|
placement: 'top',
|
|
disableBeacon: true,
|
|
},
|
|
];
|
|
|
|
export function DashboardContent() {
|
|
const [inputValue, setInputValue] = useState('');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [autoSubmit, setAutoSubmit] = useState(false);
|
|
const {
|
|
selectedAgentId,
|
|
setSelectedAgent,
|
|
initializeFromAgents,
|
|
getCurrentAgent
|
|
} = useAgentSelection();
|
|
const [initiatedThreadId, setInitiatedThreadId] = useState<string | null>(null);
|
|
const { billingError, handleBillingError, clearBillingError } =
|
|
useBillingError();
|
|
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
|
const [agentLimitData, setAgentLimitData] = useState<{
|
|
runningCount: number;
|
|
runningThreadIds: string[];
|
|
} | null>(null);
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const isMobile = useIsMobile();
|
|
const { data: accounts } = useAccounts();
|
|
const personalAccount = accounts?.find((account) => account.personal_account);
|
|
const chatInputRef = React.useRef<ChatInputHandles>(null);
|
|
const initiateAgentMutation = useInitiateAgentWithInvalidation();
|
|
const [showPaymentModal, setShowPaymentModal] = useState(false);
|
|
|
|
// Tour integration
|
|
const {
|
|
run,
|
|
stepIndex,
|
|
setStepIndex,
|
|
stopTour,
|
|
showWelcome,
|
|
handleWelcomeAccept,
|
|
handleWelcomeDecline,
|
|
} = useDashboardTour();
|
|
|
|
// Feature flag for custom agents section
|
|
const { enabled: customAgentsEnabled } = useFeatureFlag('custom_agents');
|
|
|
|
// Fetch agents to get the selected agent's name
|
|
const { data: agentsResponse } = useAgents({
|
|
limit: 100,
|
|
sort_by: 'name',
|
|
sort_order: 'asc'
|
|
});
|
|
|
|
const agents = agentsResponse?.agents || [];
|
|
const selectedAgent = selectedAgentId
|
|
? agents.find(agent => agent.agent_id === selectedAgentId)
|
|
: null;
|
|
const displayName = selectedAgent?.name || 'Suna';
|
|
const agentAvatar = undefined;
|
|
const isSunaAgent = selectedAgent?.metadata?.is_suna_default || false;
|
|
|
|
const threadQuery = useThreadQuery(initiatedThreadId || '');
|
|
|
|
const enabledEnvironment = isStagingMode() || isLocalMode();
|
|
|
|
React.useEffect(() => {
|
|
if (agents.length > 0) {
|
|
initializeFromAgents(agents, undefined, setSelectedAgent);
|
|
}
|
|
}, [agents, initializeFromAgents, setSelectedAgent]);
|
|
|
|
React.useEffect(() => {
|
|
const agentIdFromUrl = searchParams.get('agent_id');
|
|
if (agentIdFromUrl && agentIdFromUrl !== selectedAgentId) {
|
|
setSelectedAgent(agentIdFromUrl);
|
|
const newUrl = new URL(window.location.href);
|
|
newUrl.searchParams.delete('agent_id');
|
|
router.replace(newUrl.pathname + newUrl.search, { scroll: false });
|
|
}
|
|
}, [searchParams, selectedAgentId, router, setSelectedAgent]);
|
|
|
|
React.useEffect(() => {
|
|
if (threadQuery.data && initiatedThreadId) {
|
|
const thread = threadQuery.data;
|
|
if (thread.project_id) {
|
|
router.push(`/projects/${thread.project_id}/thread/${initiatedThreadId}`);
|
|
} else {
|
|
router.push(`/agents/${initiatedThreadId}`);
|
|
}
|
|
setInitiatedThreadId(null);
|
|
}
|
|
}, [threadQuery.data, initiatedThreadId, router]);
|
|
|
|
const handleTourCallback = useCallback((data: CallBackProps) => {
|
|
const { status, type, index } = data;
|
|
|
|
if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
|
|
stopTour();
|
|
} else if (type === 'step:after') {
|
|
setStepIndex(index + 1);
|
|
}
|
|
}, [stopTour, setStepIndex]);
|
|
|
|
const handleSubmit = async (
|
|
message: string,
|
|
options?: {
|
|
model_name?: string;
|
|
enable_thinking?: boolean;
|
|
reasoning_effort?: string;
|
|
stream?: boolean;
|
|
enable_context_manager?: boolean;
|
|
},
|
|
) => {
|
|
if (
|
|
(!message.trim() && !chatInputRef.current?.getPendingFiles().length) ||
|
|
isSubmitting
|
|
)
|
|
return;
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const files = chatInputRef.current?.getPendingFiles() || [];
|
|
localStorage.removeItem(PENDING_PROMPT_KEY);
|
|
|
|
const formData = new FormData();
|
|
formData.append('prompt', message);
|
|
|
|
// Add selected agent if one is chosen
|
|
if (selectedAgentId) {
|
|
formData.append('agent_id', selectedAgentId);
|
|
}
|
|
|
|
files.forEach((file, index) => {
|
|
const normalizedName = normalizeFilenameToNFC(file.name);
|
|
formData.append('files', file, normalizedName);
|
|
});
|
|
|
|
if (options?.model_name) formData.append('model_name', options.model_name);
|
|
formData.append('enable_thinking', String(options?.enable_thinking ?? false));
|
|
formData.append('reasoning_effort', options?.reasoning_effort ?? 'low');
|
|
formData.append('stream', String(options?.stream ?? true));
|
|
formData.append('enable_context_manager', String(options?.enable_context_manager ?? false));
|
|
|
|
const result = await initiateAgentMutation.mutateAsync(formData);
|
|
|
|
if (result.thread_id) {
|
|
setInitiatedThreadId(result.thread_id);
|
|
} else {
|
|
throw new Error('Agent initiation did not return a thread_id.');
|
|
}
|
|
chatInputRef.current?.clearPendingFiles();
|
|
} catch (error: any) {
|
|
console.error('Error during submission process:', error);
|
|
if (error instanceof BillingError) {
|
|
setShowPaymentModal(true);
|
|
} else if (error instanceof AgentRunLimitError) {
|
|
const { running_thread_ids, running_count } = error.detail;
|
|
setAgentLimitData({
|
|
runningCount: running_count,
|
|
runningThreadIds: running_thread_ids,
|
|
});
|
|
setShowAgentLimitDialog(true);
|
|
} else {
|
|
const errorMessage = error instanceof Error ? error.message : 'Operation failed';
|
|
toast.error(errorMessage);
|
|
}
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
const pendingPrompt = localStorage.getItem(PENDING_PROMPT_KEY);
|
|
|
|
if (pendingPrompt) {
|
|
setInputValue(pendingPrompt);
|
|
setAutoSubmit(true);
|
|
}
|
|
}, 200);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
if (autoSubmit && inputValue && !isSubmitting) {
|
|
const timer = setTimeout(() => {
|
|
handleSubmit(inputValue);
|
|
setAutoSubmit(false);
|
|
}, 500);
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [autoSubmit, inputValue, isSubmitting]);
|
|
|
|
return (
|
|
<>
|
|
<Joyride
|
|
steps={dashboardTourSteps}
|
|
run={run}
|
|
stepIndex={stepIndex}
|
|
callback={handleTourCallback}
|
|
continuous
|
|
showProgress
|
|
showSkipButton
|
|
disableOverlayClose
|
|
disableScrollParentFix
|
|
styles={{
|
|
options: {
|
|
primaryColor: '#000000',
|
|
backgroundColor: '#ffffff',
|
|
textColor: '#000000',
|
|
overlayColor: 'rgba(0, 0, 0, 0.7)',
|
|
arrowColor: '#ffffff',
|
|
zIndex: 1000,
|
|
},
|
|
tooltip: {
|
|
backgroundColor: '#ffffff',
|
|
borderRadius: 8,
|
|
fontSize: 14,
|
|
padding: 20,
|
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
border: '1px solid #e5e7eb',
|
|
},
|
|
tooltipTitle: {
|
|
color: '#000000',
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
},
|
|
tooltipContent: {
|
|
color: '#000000',
|
|
fontSize: 14,
|
|
lineHeight: 1.5,
|
|
},
|
|
buttonNext: {
|
|
backgroundColor: '#000000',
|
|
color: '#ffffff',
|
|
fontSize: 12,
|
|
padding: '8px 16px',
|
|
borderRadius: 6,
|
|
border: 'none',
|
|
fontWeight: 500,
|
|
},
|
|
buttonBack: {
|
|
color: '#6b7280',
|
|
backgroundColor: 'transparent',
|
|
fontSize: 12,
|
|
padding: '8px 16px',
|
|
border: '1px solid #e5e7eb',
|
|
borderRadius: 6,
|
|
},
|
|
buttonSkip: {
|
|
color: '#6b7280',
|
|
backgroundColor: 'transparent',
|
|
fontSize: 12,
|
|
border: 'none',
|
|
},
|
|
buttonClose: {
|
|
color: '#6b7280',
|
|
backgroundColor: 'transparent',
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<TourConfirmationDialog
|
|
open={showWelcome}
|
|
onAccept={handleWelcomeAccept}
|
|
onDecline={handleWelcomeDecline}
|
|
/>
|
|
|
|
<BillingModal
|
|
open={showPaymentModal}
|
|
onOpenChange={setShowPaymentModal}
|
|
showUsageLimitAlert={true}
|
|
/>
|
|
<div className="flex flex-col h-screen w-full overflow-hidden">
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="min-h-full flex flex-col">
|
|
{customAgentsEnabled && (
|
|
<div className="flex justify-center px-4 pt-4 md:pt-8">
|
|
<ReleaseBadge text="Custom Agents, Playbooks, and more!" link="/agents?tab=my-agents" />
|
|
</div>
|
|
)}
|
|
<div className="flex-1 flex items-center justify-center px-4 py-8">
|
|
<div className="w-full max-w-[650px] flex flex-col items-center justify-center space-y-4 md:space-y-6">
|
|
<div className="flex flex-col items-center text-center w-full">
|
|
<p
|
|
className="tracking-tight text-2xl md:text-3xl font-normal text-foreground/90"
|
|
data-tour="dashboard-title"
|
|
>
|
|
What would you like to do today?
|
|
</p>
|
|
</div>
|
|
<div className="w-full" data-tour="chat-input">
|
|
<ChatInput
|
|
ref={chatInputRef}
|
|
onSubmit={handleSubmit}
|
|
loading={isSubmitting}
|
|
placeholder="Describe what you need help with..."
|
|
value={inputValue}
|
|
onChange={setInputValue}
|
|
hideAttachments={false}
|
|
selectedAgentId={selectedAgentId}
|
|
onAgentSelect={setSelectedAgent}
|
|
enableAdvancedConfig={true}
|
|
onConfigureAgent={(agentId) => router.push(`/agents/config/${agentId}`)}
|
|
/>
|
|
</div>
|
|
<div className="w-full" data-tour="examples">
|
|
<Examples onSelectPrompt={setInputValue} count={isMobile ? 3 : 4} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{enabledEnvironment && customAgentsEnabled && (
|
|
<div className="w-full px-4 pb-8" data-tour="custom-agents">
|
|
<div className="max-w-7xl mx-auto">
|
|
<CustomAgentsSection
|
|
onAgentSelect={setSelectedAgent}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<BillingErrorAlert
|
|
message={billingError?.message}
|
|
currentUsage={billingError?.currentUsage}
|
|
limit={billingError?.limit}
|
|
accountId={personalAccount?.account_id}
|
|
onDismiss={clearBillingError}
|
|
isOpen={!!billingError}
|
|
/>
|
|
</div>
|
|
|
|
{agentLimitData && (
|
|
<AgentRunLimitDialog
|
|
open={showAgentLimitDialog}
|
|
onOpenChange={setShowAgentLimitDialog}
|
|
runningCount={agentLimitData.runningCount}
|
|
runningThreadIds={agentLimitData.runningThreadIds}
|
|
projectId={undefined}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|