R-Kentaren's picture
Upload folder using huggingface_hub
4efde5d verified
'use client';
import React, {
useState,
useRef,
useEffect,
forwardRef,
useImperativeHandle,
} from 'react';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { useAgentSelection } from '@/lib/stores/agent-selection-store';
import { Card, CardContent } from '@/components/ui/card';
import { handleFiles } from './file-upload-handler';
import { MessageInput } from './message-input';
import { AttachmentGroup } from '../attachment-group';
import { useModelSelection } from './_use-model-selection-new';
import { useFileDelete } from '@/hooks/react-query/files';
import { useQueryClient } from '@tanstack/react-query';
import { ToolCallInput } from './floating-tool-preview';
import { ChatSnack } from './chat-snack';
import { Brain, Zap, Workflow, Database, ArrowDown } from 'lucide-react';
import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
import { Skeleton } from '@/components/ui/skeleton';
import { IntegrationsRegistry } from '@/components/agents/integrations-registry';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useSubscriptionData } from '@/contexts/SubscriptionContext';
import { isLocalMode } from '@/lib/config';
import { BillingModal } from '@/components/billing/billing-modal';
import { useRouter } from 'next/navigation';
import posthog from 'posthog-js';
export interface ChatInputHandles {
getPendingFiles: () => File[];
clearPendingFiles: () => void;
}
export interface ChatInputProps {
onSubmit: (
message: string,
options?: {
model_name?: string;
enable_thinking?: boolean;
agent_id?: string;
},
) => void;
placeholder?: string;
loading?: boolean;
disabled?: boolean;
isAgentRunning?: boolean;
onStopAgent?: () => void;
autoFocus?: boolean;
value?: string;
onChange?: (value: string) => void;
onFileBrowse?: () => void;
sandboxId?: string;
hideAttachments?: boolean;
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
agentName?: string;
messages?: any[];
bgColor?: string;
toolCalls?: ToolCallInput[];
toolCallIndex?: number;
showToolPreview?: boolean;
onExpandToolPreview?: () => void;
isLoggedIn?: boolean;
enableAdvancedConfig?: boolean;
onConfigureAgent?: (agentId: string) => void;
hideAgentSelection?: boolean;
defaultShowSnackbar?: 'tokens' | 'upgrade' | false;
showToLowCreditUsers?: boolean;
agentMetadata?: {
is_suna_default?: boolean;
};
showScrollToBottomIndicator?: boolean;
onScrollToBottom?: () => void;
}
export interface UploadedFile {
name: string;
path: string;
size: number;
type: string;
localUrl?: string;
}
export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
(
{
onSubmit,
placeholder = 'Describe what you need help with...',
loading = false,
disabled = false,
isAgentRunning = false,
onStopAgent,
autoFocus = true,
value: controlledValue,
onChange: controlledOnChange,
onFileBrowse,
sandboxId,
hideAttachments = false,
selectedAgentId,
onAgentSelect,
agentName,
messages = [],
bgColor = 'bg-card',
toolCalls = [],
toolCallIndex = 0,
showToolPreview = false,
onExpandToolPreview,
isLoggedIn = true,
enableAdvancedConfig = false,
onConfigureAgent,
hideAgentSelection = false,
defaultShowSnackbar = false,
showToLowCreditUsers = true,
agentMetadata,
showScrollToBottomIndicator = false,
onScrollToBottom,
},
ref,
) => {
const isControlled =
controlledValue !== undefined && controlledOnChange !== undefined;
const router = useRouter();
const [uncontrolledValue, setUncontrolledValue] = useState('');
const value = isControlled ? controlledValue : uncontrolledValue;
const isSunaAgent = agentMetadata?.is_suna_default || false;
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const [registryDialogOpen, setRegistryDialogOpen] = useState(false);
const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar);
const [userDismissedUsage, setUserDismissedUsage] = useState(false);
const [billingModalOpen, setBillingModalOpen] = useState(false);
const {
selectedModel,
setSelectedModel: handleModelChange,
subscriptionStatus,
allModels: modelOptions,
canAccessModel,
getActualModelId,
refreshCustomModels,
} = useModelSelection();
const { data: subscriptionData } = useSubscriptionData();
const deleteFileMutation = useFileDelete();
const queryClient = useQueryClient();
// Fetch integration icons only when logged in and advanced config UI is in use
const shouldFetchIcons = isLoggedIn && !!enableAdvancedConfig;
const { data: googleDriveIcon } = useComposioToolkitIcon('googledrive', { enabled: shouldFetchIcons });
const { data: slackIcon } = useComposioToolkitIcon('slack', { enabled: shouldFetchIcons });
const { data: notionIcon } = useComposioToolkitIcon('notion', { enabled: shouldFetchIcons });
// Show usage preview logic:
// - Always show to free users when showToLowCreditUsers is true
// - For paid users, only show when they're at 70% or more of their cost limit (30% or below remaining)
const shouldShowUsage = !isLocalMode() && subscriptionData && showToLowCreditUsers && (() => {
// Free users: always show
if (subscriptionStatus === 'no_subscription') {
return true;
}
// Paid users: only show when at 70% or more of cost limit
const currentUsage = subscriptionData.current_usage || 0;
const costLimit = subscriptionData.cost_limit || 0;
if (costLimit === 0) return false; // No limit set
return currentUsage >= (costLimit * 0.7); // 70% or more used (30% or less remaining)
})();
// Auto-show usage preview when we have subscription data
useEffect(() => {
if (shouldShowUsage && defaultShowSnackbar !== false && !userDismissedUsage && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) {
setShowSnackbar('upgrade');
} else if (!shouldShowUsage && showSnackbar !== false) {
setShowSnackbar(false);
}
}, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage, subscriptionStatus, showToLowCreditUsers, userDismissedUsage]);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const { data: agentsResponse } = useAgents({}, { enabled: isLoggedIn });
const agents = agentsResponse?.agents || [];
const { initializeFromAgents } = useAgentSelection();
useImperativeHandle(ref, () => ({
getPendingFiles: () => pendingFiles,
clearPendingFiles: () => setPendingFiles([]),
}));
useEffect(() => {
if (agents.length > 0 && !onAgentSelect) {
initializeFromAgents(agents);
}
}, [agents, onAgentSelect, initializeFromAgents]);
useEffect(() => {
if (autoFocus && textareaRef.current) {
textareaRef.current.focus();
}
}, [autoFocus]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (
(!value.trim() && uploadedFiles.length === 0) ||
loading ||
(disabled && !isAgentRunning)
)
return;
if (isAgentRunning && onStopAgent) {
onStopAgent();
return;
}
let message = value;
if (uploadedFiles.length > 0) {
const fileInfo = uploadedFiles
.map((file) => `[Uploaded File: ${file.path}]`)
.join('\n');
message = message ? `${message}\n\n${fileInfo}` : fileInfo;
}
let baseModelName = getActualModelId(selectedModel);
let thinkingEnabled = false;
if (selectedModel.endsWith('-thinking')) {
baseModelName = getActualModelId(selectedModel.replace(/-thinking$/, ''));
thinkingEnabled = true;
}
posthog.capture("task_prompt_submitted", { message });
onSubmit(message, {
agent_id: selectedAgentId,
model_name: baseModelName,
enable_thinking: thinkingEnabled,
});
if (!isControlled) {
setUncontrolledValue('');
}
setUploadedFiles([]);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
if (isControlled) {
controlledOnChange(newValue);
} else {
setUncontrolledValue(newValue);
}
};
const handleTranscription = (transcribedText: string) => {
const currentValue = isControlled ? controlledValue : uncontrolledValue;
const newValue = currentValue ? `${currentValue} ${transcribedText}` : transcribedText;
if (isControlled) {
controlledOnChange(newValue);
} else {
setUncontrolledValue(newValue);
}
};
const removeUploadedFile = async (index: number) => {
const fileToRemove = uploadedFiles[index];
// Clean up local URL if it exists
if (fileToRemove.localUrl) {
URL.revokeObjectURL(fileToRemove.localUrl);
}
// Remove from local state immediately for responsive UI
setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
if (!sandboxId && pendingFiles.length > index) {
setPendingFiles((prev) => prev.filter((_, i) => i !== index));
}
// Check if file is referenced in existing chat messages before deleting from server
const isFileUsedInChat = messages.some(message => {
const content = typeof message.content === 'string' ? message.content : '';
return content.includes(`[Uploaded File: ${fileToRemove.path}]`);
});
// Only delete from server if file is not referenced in chat history
if (sandboxId && fileToRemove.path && !isFileUsedInChat) {
deleteFileMutation.mutate({
sandboxId,
filePath: fileToRemove.path,
}, {
onError: (error) => {
console.error('Failed to delete file from server:', error);
}
});
} else {
// File exists in chat history, don't delete from server
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(false);
};
return (
<div className="mx-auto w-full max-w-4xl relative">
<div className="relative">
<ChatSnack
toolCalls={toolCalls}
toolCallIndex={toolCallIndex}
onExpandToolPreview={onExpandToolPreview}
agentName={agentName}
showToolPreview={showToolPreview}
showUsagePreview={showSnackbar}
subscriptionData={subscriptionData}
onCloseUsage={() => { setShowSnackbar(false); setUserDismissedUsage(true); }}
onOpenUpgrade={() => setBillingModalOpen(true)}
isVisible={showToolPreview || !!showSnackbar}
/>
{/* Scroll to bottom button */}
{showScrollToBottomIndicator && onScrollToBottom && (
<button
onClick={onScrollToBottom}
className={`absolute cursor-pointer right-3 z-50 w-8 h-8 rounded-full bg-card border border-border transition-all duration-200 hover:scale-105 flex items-center justify-center ${showToolPreview || !!showSnackbar ? '-top-12' : '-top-5'
}`}
title="Scroll to bottom"
>
<ArrowDown className="w-4 h-4 text-muted-foreground" />
</button>
)}
<Card
className={`-mb-2 shadow-none w-full max-w-4xl mx-auto bg-transparent border-none overflow-visible ${enableAdvancedConfig && selectedAgentId ? '' : 'rounded-3xl'} relative z-10`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(false);
if (fileInputRef.current && e.dataTransfer.files.length > 0) {
const files = Array.from(e.dataTransfer.files);
handleFiles(
files,
sandboxId,
setPendingFiles,
setUploadedFiles,
setIsUploading,
messages,
queryClient,
);
}
}}
>
<div className="w-full text-sm flex flex-col justify-between items-start rounded-lg">
<CardContent className={`w-full p-1.5 pb-2 ${bgColor} border rounded-3xl`}>
<AttachmentGroup
files={uploadedFiles || []}
sandboxId={sandboxId}
onRemove={removeUploadedFile}
layout="inline"
maxHeight="216px"
showPreviews={true}
/>
<MessageInput
ref={textareaRef}
value={value}
onChange={handleChange}
onSubmit={handleSubmit}
onTranscription={handleTranscription}
placeholder={placeholder}
loading={loading}
disabled={disabled}
isAgentRunning={isAgentRunning}
onStopAgent={onStopAgent}
isDraggingOver={isDraggingOver}
uploadedFiles={uploadedFiles}
fileInputRef={fileInputRef}
isUploading={isUploading}
sandboxId={sandboxId}
setPendingFiles={setPendingFiles}
setUploadedFiles={setUploadedFiles}
setIsUploading={setIsUploading}
hideAttachments={hideAttachments}
messages={messages}
selectedModel={selectedModel}
onModelChange={handleModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
isLoggedIn={isLoggedIn}
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
hideAgentSelection={hideAgentSelection}
/>
</CardContent>
</div>
</Card>
{enableAdvancedConfig && selectedAgentId && (
<div className="w-full max-w-4xl mx-auto -mt-12 relative z-20">
<div className="bg-gradient-to-b from-transparent via-transparent to-muted/30 pt-8 pb-2 px-4 rounded-b-3xl border border-t-0 border-border/50 transition-all duration-300 ease-out">
<div className="flex items-center justify-between gap-1 overflow-x-auto scrollbar-none relative">
<button
onClick={() => setRegistryDialogOpen(true)}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-lg hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0 cursor-pointer relative pointer-events-auto"
>
<div className="flex items-center -space-x-0.5">
{googleDriveIcon?.icon_url && slackIcon?.icon_url && notionIcon?.icon_url ? (
<>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={googleDriveIcon.icon_url} className="w-2.5 h-2.5" alt="Google Drive" />
</div>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={slackIcon.icon_url} className="w-2.5 h-2.5" alt="Slack" />
</div>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={notionIcon.icon_url} className="w-2.5 h-2.5" alt="Notion" />
</div>
</>
) : (
<>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
<Skeleton className="w-2.5 h-2.5 rounded" />
</div>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
<Skeleton className="w-2.5 h-2.5 rounded" />
</div>
<div className="w-4 h-4 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
<Skeleton className="w-2.5 h-2.5 rounded" />
</div>
</>
)}
</div>
<span className="text-xs font-medium">Integrations</span>
</button>
<button
onClick={() => router.push(`/agents/config/${selectedAgentId}?tab=configuration&accordion=instructions`)}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-lg hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0 cursor-pointer relative pointer-events-auto"
>
<Brain className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Instructions</span>
</button>
<button
onClick={() => router.push(`/agents/config/${selectedAgentId}?tab=configuration&accordion=knowledge`)}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-lg hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0 cursor-pointer relative pointer-events-auto"
>
<Database className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Knowledge</span>
</button>
<button
onClick={() => router.push(`/agents/config/${selectedAgentId}?tab=configuration&accordion=triggers`)}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-lg hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0 cursor-pointer relative pointer-events-auto"
>
<Zap className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Triggers</span>
</button>
<button
onClick={() => router.push(`/agents/config/${selectedAgentId}?tab=configuration&accordion=workflows`)}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-lg hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0 cursor-pointer relative pointer-events-auto"
>
<Workflow className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Playbooks</span>
</button>
</div>
</div>
</div>
)}
<Dialog open={registryDialogOpen} onOpenChange={setRegistryDialogOpen}>
<DialogContent className="p-0 max-w-6xl h-[90vh] overflow-hidden">
<DialogHeader className="sr-only">
<DialogTitle>Integrations</DialogTitle>
</DialogHeader>
<IntegrationsRegistry
showAgentSelector={true}
selectedAgentId={selectedAgentId}
onAgentChange={onAgentSelect}
onToolsSelected={(profileId, selectedTools, appName, appSlug) => {
// Save to workflow or perform other action here
}}
/>
</DialogContent>
</Dialog>
<BillingModal
open={billingModalOpen}
onOpenChange={setBillingModalOpen}
/>
</div>
</div>
);
},
);
ChatInput.displayName = 'ChatInput';