diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index 221a73296..f37af299c 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -328,11 +328,11 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] - const handleUpgrade = async () => { + const handleUpgrade = async (planId: "api_pro" | "api_max" = "api_pro") => { setIsUpgrading(true) try { const result = await autumn.attach({ - planId: "api_pro", + planId, successUrl: window.location.href, }) if (result?.paymentUrl) { @@ -614,11 +614,21 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { )} ) : ( @@ -783,8 +793,14 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { setGranolaModalOpen(true)} + disabled={isUpgrading || autumn.isLoading} + onClick={() => { + if (!isMaxUser) { + handleUpgrade("api_max") + return + } + setGranolaModalOpen(true) + }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100 data-disabled:opacity-40 data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent" > @@ -798,7 +814,9 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { )} - Meeting notes & transcripts + {isMaxUser + ? "Meeting notes & transcripts" + : "Upgrade to Max"} diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index a19daa1e3..a2d8e3057 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -201,6 +201,7 @@ export function ChatSidebar({ const [attachmentDrafts, setAttachmentDrafts] = useState< ChatAttachmentDraft[] >([]) + const [isChatDraggingFiles, setIsChatDraggingFiles] = useState(false) const [selectedModel, setSelectedModel] = useState( initialSelectedModel ?? "grok-4.3", ) @@ -246,6 +247,7 @@ export function ChatSidebar({ null, ) const messagesContainerRef = useRef(null) + const chatDragDepthRef = useRef(0) const isScrolledToBottomRef = useRef(true) const userJustSentRef = useRef(false) const sentQueuedMessageRef = useRef(null) @@ -725,6 +727,77 @@ export function ChatSidebar({ [attachmentDrafts, currentChatId, uploadAttachmentDraft], ) + const hasDraggedFiles = useCallback( + (event: React.DragEvent) => + Array.from(event.dataTransfer.types).includes("Files"), + [], + ) + + const resetChatFileDrag = useCallback(() => { + chatDragDepthRef.current = 0 + setIsChatDraggingFiles(false) + }, []) + + const handleChatDragEnter = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + chatDragDepthRef.current += 1 + if (status !== "submitted" && status !== "streaming") { + setIsChatDraggingFiles(true) + } + }, + [hasDraggedFiles, status], + ) + + const handleChatDragOver = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + event.dataTransfer.dropEffect = + status === "submitted" || status === "streaming" ? "none" : "copy" + }, + [hasDraggedFiles, status], + ) + + const handleChatDragLeave = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + chatDragDepthRef.current = Math.max(0, chatDragDepthRef.current - 1) + if (chatDragDepthRef.current === 0) { + setIsChatDraggingFiles(false) + } + }, + [hasDraggedFiles], + ) + + const handleChatDrop = useCallback( + (event: React.DragEvent) => { + if (!hasDraggedFiles(event)) return + event.preventDefault() + event.stopPropagation() + + resetChatFileDrag() + const files = event.dataTransfer.files + if (status !== "submitted" && status !== "streaming" && files.length) { + handleAddAttachmentFiles(files) + } + }, + [handleAddAttachmentFiles, hasDraggedFiles, resetChatFileDrag, status], + ) + + useEffect(() => { + if (status === "submitted" || status === "streaming") { + resetChatFileDrag() + } + }, [resetChatFileDrag, status]) + useEffect(() => { if (pendingThreadLoad && currentChatId === pendingThreadLoad.id) { setMessages(pendingThreadLoad.messages) @@ -1765,6 +1838,27 @@ export function ChatSidebar({ ) : null + const chatDropOverlay = isChatDraggingFiles ? ( + + ) : null + const chatDropTargetProps = { + onDragEnter: handleChatDragEnter, + onDragOver: handleChatDragOver, + onDragLeave: handleChatDragLeave, + onDrop: handleChatDrop, + onDragEnd: resetChatFileDrag, + } + const shell = ( <> {showHeaderRow ? ( @@ -1983,6 +2077,7 @@ export function ChatSidebar({ onRetryAttachment={handleRetryAttachment} canSend={canSendMessage} attachmentAccept={CHAT_ATTACHMENT_ACCEPT} + disableFileDropZone sendDisabled={isResponding && isQueueFull} sendDisabledTooltip={`Queue is full (${CHAT_QUEUE_LIMIT} max)`} activeStatus={ @@ -2063,7 +2158,9 @@ export function ChatSidebar({ layout === "page" ? { opacity: 0, y: 12 } : { x: "100px", opacity: 0 } } transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }} + {...(!isPageDesktop ? chatDropTargetProps : {})} > + {!isPageDesktop && chatDropOverlay} {chatHistorySheet} {isPageDesktop ? (
@@ -2073,7 +2170,11 @@ export function ChatSidebar({ chatProject === AUTO_CHAT_SPACE_ID ? null : [chatProject] } /> -
+
+ {chatDropOverlay} {pageDesktopToolbarRow}
{shell} diff --git a/apps/web/components/chat/input/index.tsx b/apps/web/components/chat/input/index.tsx index d655d6572..0926af531 100644 --- a/apps/web/components/chat/input/index.tsx +++ b/apps/web/components/chat/input/index.tsx @@ -59,6 +59,7 @@ interface ChatInputProps { onRetryAttachment?: (id: string) => void canSend?: boolean attachmentAccept?: string + disableFileDropZone?: boolean } export default function ChatInput({ @@ -84,6 +85,7 @@ export default function ChatInput({ onRetryAttachment, canSend, attachmentAccept = CHAT_ATTACHMENT_ACCEPT, + disableFileDropZone = false, }: ChatInputProps) { const [isMultiline, setIsMultiline] = useState(false) const [isExpanded, setIsExpanded] = useState(false) @@ -200,11 +202,20 @@ export default function ChatInput({ ) : null - const dropOverlay = isDraggingFiles ? ( -
- Drop files to attach -
- ) : null + const dropOverlay = + !disableFileDropZone && isDraggingFiles ? ( +
+ Drop files to attach +
+ ) : null + const dropZoneProps = disableFileDropZone + ? {} + : { + onDragEnter: handleDragEnter, + onDragOver: handleDragOver, + onDragLeave: handleDragLeave, + onDrop: handleDrop, + } return ( {dropOverlay} @@ -368,10 +376,7 @@ export default function ChatInput({ ) : (
{ + const handleUpgrade = async (planId?: unknown) => { + const checkoutPlanId = planId === "api_max" ? "api_max" : "api_pro" try { const result = await autumn.attach({ - planId, + planId: checkoutPlanId, successUrl: `${window.location.origin}/?view=integrations`, }) if (result?.paymentUrl) { @@ -2741,7 +2742,7 @@ export function IntegrationsView({ } if (claudeCodeConnected) return if (claudeCodeNeedsPro) { - handleUpgrade() + handleUpgrade("api_pro") return } createPluginKeyMutation.mutate("claude_code") @@ -2818,7 +2819,7 @@ export function IntegrationsView({ title="Connect another agent" onClick={() => { if (needsProUpgrade) { - handleUpgrade() + handleUpgrade("api_pro") return } trackCard(item) @@ -2843,6 +2844,11 @@ export function IntegrationsView({ { trackCard(item) + if (!PLUGIN_CATALOG[item.pluginId]?.usesOAuth) { + if (connectingPlugin) return + createPluginKeyMutation.mutate(item.pluginId) + return + } setFinishSetupPluginId(item.pluginId) }} /> @@ -2850,7 +2856,7 @@ export function IntegrationsView({ } if (needsProUpgrade) { return ( - handleUpgrade()}> + handleUpgrade("api_pro")}> Upgrade ) @@ -3020,7 +3026,7 @@ export function IntegrationsView({ { if (needsProUpgrade) { - handleUpgrade() + handleUpgrade("api_pro") return } trackCard(item) @@ -3465,7 +3471,7 @@ export function IntegrationsView({
{connectedDialogNeedsPro ? ( - handleUpgrade()}> + handleUpgrade("api_pro")}> Upgrade to connect more