From 5de3af0f29e34f56d14f3cc62ac227c51477777b Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:41:59 +0530 Subject: [PATCH 01/10] improve billing page ui, add upgrade button --- apps/rowboat/app/billing/app.tsx | 47 ++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/apps/rowboat/app/billing/app.tsx b/apps/rowboat/app/billing/app.tsx index f0e6c074..ee2b6e76 100644 --- a/apps/rowboat/app/billing/app.tsx +++ b/apps/rowboat/app/billing/app.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Progress, Badge, Chip } from "@heroui/react"; +import { Progress, Badge, Chip, Spinner } from "@heroui/react"; import { Button } from "@/components/ui/button"; import { Label } from "@/app/lib/components/label"; import { Customer, UsageResponse } from "@/app/lib/types/billing_types"; @@ -11,6 +11,9 @@ import { HorizontalDivider } from "@/components/ui/horizontal-divider"; import { WithStringId } from "@/app/lib/types/types"; import clsx from 'clsx'; import { getCustomerPortalUrl } from "../actions/billing.actions"; +import { useState } from "react"; +import { ArrowUpCircle } from "lucide-react"; +import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal"; const planDetails = { free: { @@ -46,6 +49,9 @@ export function BillingPage({ customer, usage }: BillingPageProps) { const plan = customer.subscriptionPlan || "free"; const displayStatus = getDisplayStatus(customer.subscriptionStatus); const planInfo = planDetails[plan]; + const [loading, setLoading] = useState(false); + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + const [upgradeError, setUpgradeError] = useState(""); // Prepare usage metrics data const usageData = Object.entries(usage.usage) @@ -57,6 +63,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) { .sort((a, b) => b.credits - a.credits); async function handleManageSubscription() { + setLoading(true); const returnUrl = new URL('/billing/callback', window.location.origin); returnUrl.searchParams.set('redirect', window.location.href); const url = await getCustomerPortalUrl(returnUrl.toString()); @@ -105,15 +112,34 @@ export function BillingPage({ customer, usage }: BillingPageProps) { -
- + )} + {!loading && { + e.preventDefault(); + try { + await handleManageSubscription(); + } catch (err) { + setUpgradeError("Failed to open subscription portal"); + } + }} > Manage Subscription - - +
} + {loading && } + @@ -289,6 +315,11 @@ export function BillingPage({ customer, usage }: BillingPageProps) { )} + setUpgradeModalOpen(false)} + errorMessage={upgradeError} + /> ); } \ No newline at end of file From 35c2f51c8e5649d76b2054079762a8e7cc22e97d Mon Sep 17 00:00:00 2001 From: arkml Date: Tue, 19 Aug 2025 11:29:43 +0530 Subject: [PATCH 02/10] Add URL parameter handling for direct project creation (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add URL parameter handling for direct project creation - Fix projects page to handle prompt parameters and auto-redirect to build view - Add .env.swp to .gitignore to prevent accidental commits of vim swap files - Configure Inter font with fallbacks for build environments without internet access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * remove unnecessary change * fix gitignore --------- Co-authored-by: Claude --- .../components/build-assistant-section.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/apps/rowboat/app/projects/components/build-assistant-section.tsx b/apps/rowboat/app/projects/components/build-assistant-section.tsx index 7d18c0d3..4528917c 100644 --- a/apps/rowboat/app/projects/components/build-assistant-section.tsx +++ b/apps/rowboat/app/projects/components/build-assistant-section.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react"; import { listTemplates, listProjects } from "@/app/actions/project.actions"; import { createProjectWithOptions, createProjectFromJsonWithOptions, createProjectFromTemplate } from "../lib/project-creation-utils"; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import clsx from 'clsx'; import Image from 'next/image'; import mascotImage from '@/public/mascot.png'; @@ -36,6 +36,8 @@ export function BuildAssistantSection() { const [selectedTab, setSelectedTab] = useState('new'); const fileInputRef = useRef(null); const router = useRouter(); + const searchParams = useSearchParams(); + const [autoCreateLoading, setAutoCreateLoading] = useState(false); const totalPages = Math.ceil(projects.length / ITEMS_PER_PAGE); const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; @@ -103,6 +105,29 @@ export function BuildAssistantSection() { fetchProjects(); }, []); + // Handle URL parameters for auto-creation and direct redirect to build view + useEffect(() => { + const urlPrompt = searchParams.get('prompt'); + const urlTemplate = searchParams.get('template'); + + if (urlPrompt || urlTemplate) { + setAutoCreateLoading(true); + createProjectWithOptions({ + template: urlTemplate || undefined, + prompt: urlPrompt || undefined, + router, + onError: (error) => { + console.error('Error auto-creating project:', error); + setAutoCreateLoading(false); + // Fall back to showing the form with the prompt pre-filled + if (urlPrompt) { + setUserPrompt(urlPrompt); + } + } + }); + } + }, [searchParams, router]); + const handleCreateAssistant = async () => { setIsCreating(true); try { @@ -170,6 +195,15 @@ export function BuildAssistantSection() { className="hidden" onChange={handleFileChange} /> + {autoCreateLoading && ( +
+
+

+ Creating your assistant... +

+
+ )} + {!autoCreateLoading && (
{/* Main Headline */} @@ -445,6 +479,7 @@ export function BuildAssistantSection() { )}
+ )} ); } \ No newline at end of file From 105d1ae190ec3402dd41352afa804062e3460e94 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 19 Aug 2025 11:29:33 +0530 Subject: [PATCH 03/10] Merge triggers and job rules into single view and remove dropdown triggers modal --- .../job-rules/components/job-rules-tabs.tsx | 10 ++- .../components/triggers-tab.tsx} | 75 +++++++------------ .../projects/[projectId]/job-rules/page.tsx | 2 +- .../workflow/components/TopBar.tsx | 13 +--- .../[projectId]/workflow/workflow_editor.tsx | 13 ---- .../projects/layout/components/sidebar.tsx | 15 ++-- 6 files changed, 46 insertions(+), 82 deletions(-) rename apps/rowboat/app/projects/[projectId]/{workflow/components/TriggersModal.tsx => job-rules/components/triggers-tab.tsx} (88%) diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx index c5a7cf17..32d01d2d 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx @@ -4,9 +4,10 @@ import { useState } from "react"; import { Tabs, Tab } from "@/components/ui/tabs"; import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list"; import { RecurringJobRulesList } from "./recurring-job-rules-list"; +import { TriggersTab } from "./triggers-tab"; export function JobRulesTabs({ projectId }: { projectId: string }) { - const [activeTab, setActiveTab] = useState("scheduled"); + const [activeTab, setActiveTab] = useState("triggers"); const handleTabChange = (key: React.Key) => { setActiveTab(key.toString()); @@ -20,10 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) { aria-label="Job Rules" fullWidth > - + + + + - + diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx similarity index 88% rename from apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx rename to apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx index 3ec1a906..9d7d9afe 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx @@ -1,36 +1,23 @@ 'use client'; import React, { useState, useEffect, useCallback } from 'react'; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react'; +import { Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react'; import { Plus, Trash2, ZapIcon } from 'lucide-react'; import { z } from 'zod'; import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions'; import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; -import { ComposioTriggerTypesPanel } from './ComposioTriggerTypesPanel'; -import { TriggerConfigForm } from './TriggerConfigForm'; +import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; +import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal'; import { ZToolkit } from "@/src/application/lib/composio/types"; import { Project } from "@/src/entities/models/project"; - -interface TriggersModalProps { - isOpen: boolean; - onClose: () => void; - projectId: string; - projectConfig: z.infer; - onProjectConfigUpdated?: () => void; -} +import { fetchProject } from '@/app/actions/project.actions'; type TriggerDeployment = z.infer; -export function TriggersModal({ - isOpen, - onClose, - projectId, - projectConfig, - onProjectConfigUpdated, -}: TriggersModalProps) { +export function TriggersTab({ projectId }: { projectId: string }) { const [triggers, setTriggers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -40,6 +27,16 @@ export function TriggersModal({ const [showAuthModal, setShowAuthModal] = useState(false); const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false); const [deletingTrigger, setDeletingTrigger] = useState(null); + const [projectConfig, setProjectConfig] = useState | null>(null); + + const loadProjectConfig = useCallback(async () => { + try { + const config = await fetchProject(projectId); + setProjectConfig(config); + } catch (err: any) { + console.error('Error fetching project config:', err); + } + }, [projectId]); const loadTriggers = useCallback(async () => { try { @@ -115,7 +112,7 @@ export function TriggersModal({ const handleAuthComplete = async () => { setShowAuthModal(false); - onProjectConfigUpdated?.(); + await loadProjectConfig(); // Refresh project config }; const handleTriggerSubmit = async (triggerConfig: Record) => { @@ -151,10 +148,14 @@ export function TriggersModal({ }; useEffect(() => { - if (isOpen && !showCreateFlow) { + loadProjectConfig(); + }, [loadProjectConfig]); + + useEffect(() => { + if (!showCreateFlow) { loadTriggers(); } - }, [isOpen, showCreateFlow, loadTriggers]); + }, [showCreateFlow, loadTriggers]); const renderTriggerList = () => { if (loading) { @@ -319,31 +320,11 @@ export function TriggersModal({ return ( <> - - - -
- - Manage Triggers -
-
- - {showCreateFlow ? renderCreateFlow() : renderTriggerList()} - - {!showCreateFlow && ( - - - - )} -
-
+
+
+ {showCreateFlow ? renderCreateFlow() : renderTriggerList()} +
+
{/* Auth Modal */} {selectedToolkit && ( @@ -357,4 +338,4 @@ export function TriggersModal({ )} ); -} \ No newline at end of file +} diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx index a6149b91..b7ce6e4c 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/page.tsx @@ -3,7 +3,7 @@ import { requireActiveBillingSubscription } from '@/app/lib/billing'; import { JobRulesTabs } from "./components/job-rules-tabs"; export const metadata: Metadata = { - title: "Job Rules", + title: "Triggers", }; export default async function Page( diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx index 1fcb772f..4a948f50 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx @@ -8,7 +8,7 @@ interface TopBarProps { localProjectName: string; projectNameError: string | null; onProjectNameChange: (value: string) => void; - onProjectNameCommit: (value: string) => void; + onProjectNameCommit: (value: string) => Promise; publishing: boolean; isLive: boolean; showCopySuccess: boolean; @@ -23,7 +23,6 @@ interface TopBarProps { onRevertToLive: () => void; onToggleCopilot: () => void; onSettingsModalOpen: () => void; - onTriggersModalOpen: () => void; } export function TopBar({ @@ -45,7 +44,6 @@ export function TopBar({ onRevertToLive, onToggleCopilot, onSettingsModalOpen, - onTriggersModalOpen, }: TopBarProps) { const router = useRouter(); const params = useParams(); @@ -168,16 +166,9 @@ export function TopBar({ } - onPress={onTriggersModalOpen} - > - Manage triggers - - } onPress={() => { if (projectId) { router.push(`/projects/${projectId}/job-rules`); } }} > - Go to schedule runs + Manage triggers {!isLive ? ( <> diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 1ca97033..f677f96d 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -37,7 +37,6 @@ import { Button as CustomButton } from "@/components/ui/button"; import { ConfigApp } from "../config/app"; import { InputField } from "@/app/lib/components/input-field"; import { VoiceSection } from "../config/components/voice"; -import { TriggersModal } from "./components/TriggersModal"; import { TopBar } from "./components/TopBar"; enablePatches(); @@ -882,9 +881,6 @@ export function WorkflowEditor({ // Modal state for chat widget configuration const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure(); - // Modal state for triggers management - const { isOpen: isTriggersModalOpen, onOpen: onTriggersModalOpen, onClose: onTriggersModalClose } = useDisclosure(); - // Project name state const [localProjectName, setLocalProjectName] = useState(projectConfig.name || ''); const [projectNameError, setProjectNameError] = useState(null); @@ -1285,7 +1281,6 @@ export function WorkflowEditor({ onRevertToLive={handleRevertToLive} onToggleCopilot={() => setShowCopilot(!showCopilot)} onSettingsModalOpen={onSettingsModalOpen} - onTriggersModalOpen={onTriggersModalOpen} /> {/* Content Area */} @@ -1565,14 +1560,6 @@ export function WorkflowEditor({ */} - {/* Triggers Management Modal */} - ); diff --git a/apps/rowboat/app/projects/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx index 25085c69..53b6192e 100644 --- a/apps/rowboat/app/projects/layout/components/sidebar.tsx +++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx @@ -17,7 +17,8 @@ import { HelpCircle, MessageSquareIcon, LogsIcon, - Clock + Clock, + ZapIcon } from "lucide-react"; import { fetchProject } from "@/app/actions/project.actions"; import { createProjectWithOptions } from "../../lib/project-creation-utils"; @@ -102,6 +103,12 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl icon: WorkflowIcon, requiresProject: true }, + { + href: 'job-rules', + label: 'Triggers', + icon: ZapIcon, + requiresProject: true + }, { href: 'conversations', label: 'Conversations', @@ -114,12 +121,6 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl icon: LogsIcon, requiresProject: true }, - { - href: 'job-rules', - label: 'Job Rules', - icon: Clock, - requiresProject: true - }, { href: 'config', label: 'Settings', From d59235408043491986439f7826db0c2965e30ca5 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:46:47 +0530 Subject: [PATCH 04/10] fix rowboat logo --- .../projects/layout/components/sidebar.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/rowboat/app/projects/layout/components/sidebar.tsx b/apps/rowboat/app/projects/layout/components/sidebar.tsx index 53b6192e..bd1a92c3 100644 --- a/apps/rowboat/app/projects/layout/components/sidebar.tsx +++ b/apps/rowboat/app/projects/layout/components/sidebar.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import Link from "next/link"; import Image from "next/image"; -import logoImage from '@/public/logo-only.png'; +import logo from '@/public/logo.png'; +import logoOnly from '@/public/logo-only.png'; import { usePathname } from "next/navigation"; import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"; import { UserButton } from "@/app/lib/components/user_button"; @@ -155,18 +156,17 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl ${collapsed ? 'py-3' : 'gap-3 px-4 py-2.5 justify-start'} `} > - Rowboat - {!collapsed && ( - - Rowboat - - )} + width={24} + height={24} + />} + {!collapsed && Rowboat} From e4486010465ad63d60a61320b72a7e110f2a7e2c Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 19 Aug 2025 13:03:39 +0530 Subject: [PATCH 05/10] Update all triggers to display standard cards for existing triggers with delete buttons --- .../job-rules/components/job-rules-tabs.tsx | 6 +- .../components/recurring-job-rules-list.tsx | 96 +++-- .../job-rules/components/triggers-tab.tsx | 349 +++++++++++++----- .../components/scheduled-job-rules-list.tsx | 83 +++-- 4 files changed, 381 insertions(+), 153 deletions(-) diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx index 32d01d2d..7afb463a 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/job-rules-tabs.tsx @@ -21,13 +21,13 @@ export function JobRulesTabs({ projectId }: { projectId: string }) { aria-label="Job Rules" fullWidth > - + - + - + diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx index eeb12d12..008ae461 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rules-list.tsx @@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, Spinner } from "@heroui/react"; import { Button } from "@/components/ui/button"; import { Panel } from "@/components/common/panel-common"; -import { listRecurringJobRules } from "@/app/actions/recurring-job-rules.actions"; +import { listRecurringJobRules, deleteRecurringJobRule } from "@/app/actions/recurring-job-rules.actions"; import { z } from "zod"; import { ListedRecurringRuleItem } from "@/src/application/repositories/recurring-job-rules.repository.interface"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; -import { PlusIcon } from "lucide-react"; +import { PlusIcon, Trash2 } from "lucide-react"; type ListedItem = z.infer; @@ -18,6 +18,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) { const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(false); + const [deletingRule, setDeletingRule] = useState(null); const fetchPage = useCallback(async (cursorArg?: string | null) => { const res = await listRecurringJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 }); @@ -48,6 +49,24 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) { setLoadingMore(false); }, [cursor, fetchPage]); + const handleDeleteRule = async (ruleId: string) => { + if (!window.confirm('Are you sure you want to delete this recurring trigger?')) { + return; + } + + try { + setDeletingRule(ruleId); + await deleteRecurringJobRule({ projectId, ruleId }); + // Remove the deleted item from the list + setItems(prev => prev.filter(item => item.id !== ruleId)); + } catch (err: any) { + console.error('Error deleting recurring trigger:', err); + alert('Failed to delete recurring trigger. Please try again.'); + } finally { + setDeletingRule(null); + } + }; + const sections = useMemo(() => { const groups: Record = { Today: [], @@ -109,18 +128,15 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) { return ( -
- RECURRING JOB RULES -
+
+ Run your assistant workflow on an automated repeating schedule (cron jobs).
} rightActions={
-
@@ -145,38 +161,48 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
{sectionItems.map((item) => ( - -
+
-
- - {getStatusText(item.disabled, item.lastError || null)} - - - Next run: {formatNextRunAt(item.nextRunAt)} - -
-
- Schedule: {formatCronExpression(item.cron)} -
-
- Created: {new Date(item.createdAt).toLocaleDateString()} -
- {item.lastError && ( -
- Last error: {item.lastError} + +
+ + {getStatusText(item.disabled, item.lastError || null)} + + + Next run: {formatNextRunAt(item.nextRunAt)} +
- )} -
-
- {new Date(item.createdAt).toLocaleDateString()} +
+ Schedule: {formatCronExpression(item.cron)} +
+
+ Created: {new Date(item.createdAt).toLocaleDateString()} +
+ {item.lastError && ( +
+ Last error: {item.lastError} +
+ )} +
+
- +
))}
@@ -184,7 +210,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) { })} {items.length === 0 && !loading && (
- No recurring job rules found. Create your first rule to get started. + No recurring triggers yet. Create your first recurring trigger to get started.
)} {hasMore && ( diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx index 9d7d9afe..ec0f2922 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx @@ -1,12 +1,15 @@ 'use client'; -import React, { useState, useEffect, useCallback } from 'react'; -import { Button, Spinner, Card, CardBody, CardHeader } from '@heroui/react'; -import { Plus, Trash2, ZapIcon } from 'lucide-react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Spinner } from '@heroui/react'; +import { Button } from '@/components/ui/button'; +import { Panel } from '@/components/common/panel-common'; +import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react'; import { z } from 'zod'; import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; -import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions'; +import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date'; +import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions'; import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; @@ -28,6 +31,11 @@ export function TriggersTab({ projectId }: { projectId: string }) { const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false); const [deletingTrigger, setDeletingTrigger] = useState(null); const [projectConfig, setProjectConfig] = useState | null>(null); + const [triggerTypeNames, setTriggerTypeNames] = useState>({}); + const [expandedTrigger, setExpandedTrigger] = useState(null); + const [cursor, setCursor] = useState(null); + const [hasMore, setHasMore] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); const loadProjectConfig = useCallback(async () => { try { @@ -38,12 +46,56 @@ export function TriggersTab({ projectId }: { projectId: string }) { } }, [projectId]); + const loadTriggerTypeNames = useCallback(async () => { + try { + const names: Record = {}; + + // Get unique toolkit slugs from existing triggers + const uniqueToolkits = [...new Set(triggers.map(t => t.toolkitSlug))]; + + // Fetch trigger types for each toolkit + for (const toolkitSlug of uniqueToolkits) { + try { + const response = await listComposioTriggerTypes(toolkitSlug); + response.items.forEach(triggerType => { + names[triggerType.slug] = triggerType.name; + }); + } catch (err) { + console.error(`Error fetching trigger types for ${toolkitSlug}:`, err); + } + } + + setTriggerTypeNames(names); + } catch (err: any) { + console.error('Error loading trigger type names:', err); + } + }, [triggers]); + + const sections = useMemo(() => { + const groups: Record = { + Today: [], + 'This week': [], + 'This month': [], + Older: [], + }; + for (const trigger of triggers) { + const d = new Date(trigger.createdAt); + if (isToday(d)) groups['Today'].push(trigger); + else if (isThisWeek(d)) groups['This week'].push(trigger); + else if (isThisMonth(d)) groups['This month'].push(trigger); + else groups['Older'].push(trigger); + } + return groups; + }, [triggers]); + const loadTriggers = useCallback(async () => { try { setLoading(true); setError(null); const response = await listComposioTriggerDeployments({ projectId }); setTriggers(response.items); + setCursor(response.nextCursor); + setHasMore(Boolean(response.nextCursor)); } catch (err: any) { console.error('Error loading triggers:', err); setError('Failed to load triggers. Please try again.'); @@ -52,6 +104,21 @@ export function TriggersTab({ projectId }: { projectId: string }) { } }, [projectId]); + const loadMore = useCallback(async () => { + if (!cursor) return; + setLoadingMore(true); + try { + const response = await listComposioTriggerDeployments({ projectId, cursor }); + setTriggers(prev => [...prev, ...response.items]); + setCursor(response.nextCursor); + setHasMore(Boolean(response.nextCursor)); + } catch (err: any) { + console.error('Error loading more triggers:', err); + } finally { + setLoadingMore(false); + } + }, [cursor, projectId]); + const handleDeleteTrigger = async (deploymentId: string) => { if (!window.confirm('Are you sure you want to delete this trigger?')) { return; @@ -79,6 +146,7 @@ export function TriggersTab({ projectId }: { projectId: string }) { setSelectedTriggerType(null); setShowAuthModal(false); setIsSubmittingTrigger(false); + setExpandedTrigger(null); // Reset expanded state loadTriggers(); // Reload in case any triggers were created }; @@ -157,106 +225,217 @@ export function TriggersTab({ projectId }: { projectId: string }) { } }, [showCreateFlow, loadTriggers]); + useEffect(() => { + if (triggers.length > 0) { + loadTriggerTypeNames(); + } + }, [triggers, loadTriggerTypeNames]); + const renderTriggerList = () => { if (loading) { return ( -
- - Loading triggers... -
+ + Loading your triggers + + } + > +
+
+
+ + Loading triggers... +
+
+
+
); } if (error) { return ( -
-

{error}

- -
+ + Error loading your triggers + + } + rightActions={ + + } + > +
+
+
+

{error}

+
+
+
+
); } if (triggers.length === 0) { return ( -
- -

- No triggers configured -

-

- Set up your first trigger to listen for events from your connected apps. -

- -
+ + Listen for events from connected apps to run your assistant workflow automatically. + + } + rightActions={ + + } + > +
+
+
+ +

+ No external triggers yet +

+

+ Create your first external trigger to listen for events from your connected apps. +

+
+
+
+
); } return ( -
-
-

- Active Triggers ({triggers.length}) -

+ + Listen for events from connected apps to run your assistant workflow automatically. +
+ } + rightActions={ -
- -
- {triggers.map((trigger) => ( - - -
-

- {trigger.triggerTypeSlug} -

-

- Created {new Date(trigger.createdAt).toLocaleDateString()} -

-
- -
- -
-

Trigger ID: {trigger.triggerId}

-

Connected Account: {trigger.connectedAccountId}

- {Object.keys(trigger.triggerConfig).length > 0 && ( -
- Configuration: -
-                        {JSON.stringify(trigger.triggerConfig, null, 2)}
-                      
+ } + > +
+
+
+ {Object.entries(sections).map(([sectionName, sectionTriggers]) => { + if (sectionTriggers.length === 0) return null; + return ( +
+

+ {sectionName} +

+
+ {sectionTriggers.map((trigger) => ( +
+
+
+
+ + Active + + + {triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug} + +
+
+ Created: {new Date(trigger.createdAt).toLocaleDateString()} +
+ {Object.keys(trigger.triggerConfig).length > 0 && ( +
+ Configuration: {Object.keys(trigger.triggerConfig).length} settings +
+ )} +
+ +
+ + {/* Advanced Details Section - Collapsible */} +
+ + + {expandedTrigger === trigger.id && ( +
+
+ Slug: {trigger.triggerTypeSlug} +
+
+ Trigger ID: {trigger.triggerId} +
+
+ Connected Account: {trigger.connectedAccountId} +
+
+ )} +
+
+ ))}
- )} +
+ ); + })} + + {hasMore && ( +
+
- - - ))} + )} +
+
-
+ ); }; @@ -288,8 +467,8 @@ export function TriggersTab({ projectId }: { projectId: string }) { Select a Toolkit to Create Trigger @@ -320,11 +499,7 @@ export function TriggersTab({ projectId }: { projectId: string }) { return ( <> -
-
- {showCreateFlow ? renderCreateFlow() : renderTriggerList()} -
-
+ {showCreateFlow ? renderCreateFlow() : renderTriggerList()} {/* Auth Modal */} {selectedToolkit && ( diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx index 24ee11a9..da980452 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/scheduled/components/scheduled-job-rules-list.tsx @@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, Spinner } from "@heroui/react"; import { Button } from "@/components/ui/button"; import { Panel } from "@/components/common/panel-common"; -import { listScheduledJobRules } from "@/app/actions/scheduled-job-rules.actions"; +import { listScheduledJobRules, deleteScheduledJobRule } from "@/app/actions/scheduled-job-rules.actions"; import { z } from "zod"; import { ListedRuleItem } from "@/src/application/repositories/scheduled-job-rules.repository.interface"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; -import { PlusIcon } from "lucide-react"; +import { PlusIcon, Trash2 } from "lucide-react"; type ListedItem = z.infer; @@ -18,6 +18,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) { const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(false); + const [deletingRule, setDeletingRule] = useState(null); const fetchPage = useCallback(async (cursorArg?: string | null) => { const res = await listScheduledJobRules({ projectId, cursor: cursorArg ?? undefined, limit: 20 }); @@ -48,6 +49,24 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) { setLoadingMore(false); }, [cursor, fetchPage]); + const handleDeleteRule = async (ruleId: string) => { + if (!window.confirm('Are you sure you want to delete this one-time trigger?')) { + return; + } + + try { + setDeletingRule(ruleId); + await deleteScheduledJobRule({ projectId, ruleId }); + // Remove the deleted item from the list + setItems(prev => prev.filter(item => item.id !== ruleId)); + } catch (err: any) { + console.error('Error deleting one-time trigger:', err); + alert('Failed to delete one-time trigger. Please try again.'); + } finally { + setDeletingRule(null); + } + }; + const sections = useMemo(() => { const groups: Record = { Today: [], @@ -87,18 +106,15 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) { return ( -
- SCHEDULED JOB RULES -
+
+ Schedule a single job to run your assistant workflow at a specific date and time.
} rightActions={
-
@@ -123,30 +139,40 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
{sectionItems.map((item) => ( - -
+
-
- - {getStatusText(item.status, item.processedAt || null)} - - - Next run: {formatNextRunAt(item.nextRunAt)} - -
-
- Created: {new Date(item.createdAt).toLocaleDateString()} -
-
-
- {new Date(item.createdAt).toLocaleDateString()} + +
+ + {getStatusText(item.status, item.processedAt || null)} + + + Next run: {formatNextRunAt(item.nextRunAt)} + +
+
+ Created: {new Date(item.createdAt).toLocaleDateString()} +
+
+
- +
))}
@@ -154,7 +180,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) { })} {items.length === 0 && !loading && (
- No scheduled job rules found. Create your first rule to get started. + No one-time triggers yet. Create your first one-time trigger to get started.
)} {hasMore && ( @@ -183,3 +209,4 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) { ); } + From eab27b120649991782170203f0f2e737b76737a2 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 19 Aug 2025 13:20:21 +0530 Subject: [PATCH 06/10] Show jobs view for composio triggers (external) similar to recurring jobs view --- apps/rowboat/app/actions/composio.actions.ts | 11 ++ .../composio-trigger-deployment-view.tsx | 171 ++++++++++++++++++ .../job-rules/components/triggers-tab.tsx | 35 ++-- .../triggers/[deploymentId]/page.tsx | 19 ++ .../[projectId]/jobs/components/job-view.tsx | 2 +- .../[projectId]/jobs/components/jobs-list.tsx | 2 +- apps/rowboat/di/container.ts | 4 + ...ch-composio-trigger-deployment.use-case.ts | 62 +++++++ ...-composio-trigger-deployment.controller.ts | 44 +++++ 9 files changed, 332 insertions(+), 18 deletions(-) create mode 100644 apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx create mode 100644 apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx create mode 100644 apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts create mode 100644 apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts index 15411014..36741199 100644 --- a/apps/rowboat/app/actions/composio.actions.ts +++ b/apps/rowboat/app/actions/composio.actions.ts @@ -13,6 +13,7 @@ import { ICreateComposioTriggerDeploymentController } from "@/src/interface-adap import { IListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; import { IDeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { IListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; +import { IFetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; import { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; import { authCheck } from "./auth.actions"; import { ICreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; @@ -26,6 +27,7 @@ const createComposioTriggerDeploymentController = container.resolve("listComposioTriggerDeploymentsController"); const deleteComposioTriggerDeploymentController = container.resolve("deleteComposioTriggerDeploymentController"); const listComposioTriggerTypesController = container.resolve("listComposioTriggerTypesController"); +const fetchComposioTriggerDeploymentController = container.resolve("fetchComposioTriggerDeploymentController"); const deleteComposioConnectedAccountController = container.resolve("deleteComposioConnectedAccountController"); const createComposioManagedConnectedAccountController = container.resolve("createComposioManagedConnectedAccountController"); const createCustomConnectedAccountController = container.resolve("createCustomConnectedAccountController"); @@ -182,4 +184,13 @@ export async function deleteComposioTriggerDeployment(request: { projectId: request.projectId, deploymentId: request.deploymentId, }); +} + +export async function fetchComposioTriggerDeployment(request: { deploymentId: string }) { + const user = await authCheck(); + return await fetchComposioTriggerDeploymentController.execute({ + caller: 'user', + userId: user._id, + deploymentId: request.deploymentId, + }); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx new file mode 100644 index 00000000..19d54c0c --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/composio-trigger-deployment-view.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { Spinner } from "@heroui/react"; +import { Panel } from "@/components/common/panel-common"; +import { Button } from "@/components/ui/button"; +import { ArrowLeftIcon, Trash2Icon } from "lucide-react"; +import { z } from "zod"; +import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment"; +import { deleteComposioTriggerDeployment, fetchComposioTriggerDeployment } from "@/app/actions/composio.actions"; +import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list"; +import { JobFiltersSchema } from "@/src/application/repositories/jobs.repository.interface"; + +export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { projectId: string; deploymentId: string; }) { + const [deployment, setDeployment] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [deleting, setDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + const jobsFilters = useMemo(() => ({ composioTriggerDeploymentId: deploymentId } satisfies z.infer), [deploymentId]); + + useEffect(() => { + let ignore = false; + (async () => { + setLoading(true); + try { + const res = await fetchComposioTriggerDeployment({ deploymentId }); + if (ignore) return; + setDeployment(res); + } finally { + if (!ignore) setLoading(false); + } + })(); + return () => { ignore = true; }; + }, [deploymentId]); + + const title = useMemo(() => { + if (!deployment) return 'External Trigger'; + return `External Trigger ${deployment.id}`; + }, [deployment]); + + const formatDate = (iso: string) => new Date(iso).toLocaleString(); + + const handleDelete = async () => { + if (!deployment) return; + setDeleting(true); + try { + await deleteComposioTriggerDeployment({ projectId, deploymentId: deployment.id }); + window.location.href = `/projects/${projectId}/job-rules`; + } catch (e) { + console.error(e); + alert('Failed to delete trigger'); + } finally { + setDeleting(false); + setShowDeleteConfirm(false); + } + }; + + return ( + <> + + + + +
{title}
+
+ } + rightActions={ +
+ +
+ } + > +
+
+ {loading && ( +
+ +
Loading...
+
+ )} + {!loading && deployment && ( +
+
+
+
+ Deployment ID: + {deployment.id} +
+
+ Trigger Type: + {deployment.triggerTypeSlug} +
+
+ Toolkit: + {deployment.toolkitSlug} +
+
+ Connected Account: + {deployment.connectedAccountId} +
+
+ Created: + {formatDate(deployment.createdAt)} +
+
+ Updated: + {formatDate(deployment.updatedAt)} +
+
+ Trigger Config: +
+{JSON.stringify(deployment.triggerConfig, null, 2)}
+                                            
+
+
+
+ +
+

Jobs Created by This Trigger

+ +
+
+ )} + {!loading && !deployment && ( +
+
Trigger deployment not found.
+
+ )} +
+
+
+ + {showDeleteConfirm && ( +
+
+

Delete External Trigger

+

Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.

+
+ + +
+
+
+ )} + + ); +} + + diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx index ec0f2922..2ed1c3f3 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Spinner } from '@heroui/react'; +import { Spinner, Link } from '@heroui/react'; import { Button } from '@/components/ui/button'; import { Panel } from '@/components/common/panel-common'; import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react'; @@ -350,22 +350,24 @@ export function TriggersTab({ projectId }: { projectId: string }) { >
-
- - Active - - - {triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug} - -
-
- Created: {new Date(trigger.createdAt).toLocaleDateString()} -
- {Object.keys(trigger.triggerConfig).length > 0 && ( -
+
)} diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx new file mode 100644 index 00000000..b64b2787 --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/job-rules/triggers/[deploymentId]/page.tsx @@ -0,0 +1,19 @@ +import { Metadata } from "next"; +import { requireActiveBillingSubscription } from '@/app/lib/billing'; +import { ComposioTriggerDeploymentView } from "../../components/composio-trigger-deployment-view"; + +export const metadata: Metadata = { + title: "External Trigger", +}; + +export default async function Page( + props: { + params: Promise<{ projectId: string; deploymentId: string }> + } +) { + const params = await props.params; + await requireActiveBillingSubscription(); + return ; +} + + diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx index a41be6fe..c39b941e 100644 --- a/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/jobs/components/job-view.tsx @@ -55,7 +55,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string 'Deployment ID': reason.triggerDeploymentId, }, payload: reason.payload, - link: null + link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null }; } if (reason.type === 'scheduled_job_rule') { diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx index a1d5f445..36b9d06d 100644 --- a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx @@ -99,7 +99,7 @@ export function JobsList({ projectId, filters, showTitle = true, customTitle }: return { type: 'Composio Trigger', display: `Composio: ${reason.triggerTypeSlug}`, - link: null + link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null }; } if (reason.type === 'scheduled_job_rule') { diff --git a/apps/rowboat/di/container.ts b/apps/rowboat/di/container.ts index 294a3158..f2af8a95 100644 --- a/apps/rowboat/di/container.ts +++ b/apps/rowboat/di/container.ts @@ -23,6 +23,7 @@ import { MongodbProjectsRepository } from "@/src/infrastructure/repositories/mon import { MongodbComposioTriggerDeploymentsRepository } from "@/src/infrastructure/repositories/mongodb.composio-trigger-deployments.repository"; import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case"; import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case"; +import { FetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case"; import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case"; import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case"; @@ -30,6 +31,7 @@ import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; +import { FetchComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller"; import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller"; import { JobsWorker } from "@/src/application/workers/jobs.worker"; @@ -299,10 +301,12 @@ container.register({ listComposioTriggerTypesUseCase: asClass(ListComposioTriggerTypesUseCase).singleton(), createComposioTriggerDeploymentUseCase: asClass(CreateComposioTriggerDeploymentUseCase).singleton(), listComposioTriggerDeploymentsUseCase: asClass(ListComposioTriggerDeploymentsUseCase).singleton(), + fetchComposioTriggerDeploymentUseCase: asClass(FetchComposioTriggerDeploymentUseCase).singleton(), deleteComposioTriggerDeploymentUseCase: asClass(DeleteComposioTriggerDeploymentUseCase).singleton(), createComposioTriggerDeploymentController: asClass(CreateComposioTriggerDeploymentController).singleton(), deleteComposioTriggerDeploymentController: asClass(DeleteComposioTriggerDeploymentController).singleton(), listComposioTriggerDeploymentsController: asClass(ListComposioTriggerDeploymentsController).singleton(), + fetchComposioTriggerDeploymentController: asClass(FetchComposioTriggerDeploymentController).singleton(), listComposioTriggerTypesController: asClass(ListComposioTriggerTypesController).singleton(), // conversations diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts new file mode 100644 index 00000000..ed0be2de --- /dev/null +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case.ts @@ -0,0 +1,62 @@ +import { NotFoundError } from '@/src/entities/errors/common'; +import { z } from "zod"; +import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface'; +import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy'; +import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface'; +import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string().optional(), + apiKey: z.string().optional(), + deploymentId: z.string(), +}); + +export interface IFetchComposioTriggerDeploymentUseCase { + execute(request: z.infer): Promise>; +} + +export class FetchComposioTriggerDeploymentUseCase implements IFetchComposioTriggerDeploymentUseCase { + private readonly composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository; + private readonly usageQuotaPolicy: IUsageQuotaPolicy; + private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy; + + constructor({ + composioTriggerDeploymentsRepository, + usageQuotaPolicy, + projectActionAuthorizationPolicy, + }: { + composioTriggerDeploymentsRepository: IComposioTriggerDeploymentsRepository, + usageQuotaPolicy: IUsageQuotaPolicy, + projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, + }) { + this.composioTriggerDeploymentsRepository = composioTriggerDeploymentsRepository; + this.usageQuotaPolicy = usageQuotaPolicy; + this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy; + } + + async execute(request: z.infer): Promise> { + // fetch deployment first to get projectId + const deployment = await this.composioTriggerDeploymentsRepository.fetch(request.deploymentId); + if (!deployment) { + throw new NotFoundError(`Composio trigger deployment ${request.deploymentId} not found`); + } + + const { projectId } = deployment; + + // authz check + await this.projectActionAuthorizationPolicy.authorize({ + caller: request.caller, + userId: request.userId, + apiKey: request.apiKey, + projectId, + }); + + // assert and consume quota + await this.usageQuotaPolicy.assertAndConsume(projectId); + + return deployment; + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts new file mode 100644 index 00000000..a630549b --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/fetch-composio-trigger-deployment.controller.ts @@ -0,0 +1,44 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IFetchComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/fetch-composio-trigger-deployment.use-case"; +import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment"; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string().optional(), + apiKey: z.string().optional(), + deploymentId: z.string(), +}); + +export interface IFetchComposioTriggerDeploymentController { + execute(request: z.infer): Promise>; +} + +export class FetchComposioTriggerDeploymentController implements IFetchComposioTriggerDeploymentController { + private readonly fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase; + + constructor({ + fetchComposioTriggerDeploymentUseCase, + }: { + fetchComposioTriggerDeploymentUseCase: IFetchComposioTriggerDeploymentUseCase, + }) { + this.fetchComposioTriggerDeploymentUseCase = fetchComposioTriggerDeploymentUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + const { caller, userId, apiKey, deploymentId } = result.data; + + return await this.fetchComposioTriggerDeploymentUseCase.execute({ + caller, + userId, + apiKey, + deploymentId, + }); + } +} + + From 63564810e8f0efa30ba3586d0a65d762d7db9111 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:34:49 +0530 Subject: [PATCH 07/10] store friendly name in composio trigger deploys --- apps/rowboat/app/actions/composio.actions.ts | 4 +-- .../job-rules/components/triggers-tab.tsx | 1 - .../src/application/lib/composio/composio.ts | 5 ++++ ...rigger-deployments.repository.interface.ts | 1 + ...te-composio-trigger-deployment.use-case.ts | 28 +++++++++++-------- .../models/composio-trigger-deployment.ts | 1 + ...-composio-trigger-deployment.controller.ts | 12 ++++---- 7 files changed, 32 insertions(+), 20 deletions(-) diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts index 36741199..5609cf3c 100644 --- a/apps/rowboat/app/actions/composio.actions.ts +++ b/apps/rowboat/app/actions/composio.actions.ts @@ -135,7 +135,6 @@ export async function listComposioTriggerTypes(toolkitSlug: string, cursor?: str export async function createComposioTriggerDeployment(request: { projectId: string, - toolkitSlug: string, triggerTypeSlug: string, connectedAccountId: string, triggerConfig?: Record, @@ -146,9 +145,8 @@ export async function createComposioTriggerDeployment(request: { return await createComposioTriggerDeploymentController.execute({ caller: 'user', userId: user._id, + projectId: request.projectId, data: { - projectId: request.projectId, - toolkitSlug: request.toolkitSlug, triggerTypeSlug: request.triggerTypeSlug, connectedAccountId: request.connectedAccountId, triggerConfig: request.triggerConfig ?? {}, diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx index 2ed1c3f3..a68594db 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/triggers-tab.tsx @@ -199,7 +199,6 @@ export function TriggersTab({ projectId }: { projectId: string }) { // Create the trigger deployment await createComposioTriggerDeployment({ projectId, - toolkitSlug: selectedToolkit.slug, triggerTypeSlug: selectedTriggerType.slug, connectedAccountId, triggerConfig, diff --git a/apps/rowboat/src/application/lib/composio/composio.ts b/apps/rowboat/src/application/lib/composio/composio.ts index 0b3e8a19..057a14cc 100644 --- a/apps/rowboat/src/application/lib/composio/composio.ts +++ b/apps/rowboat/src/application/lib/composio/composio.ts @@ -212,4 +212,9 @@ export async function listTriggersTypes(toolkitSlug: string, cursor?: string): P // fetch return composioApiCall(ZListResponse(ZTriggerType), url.toString()); +} + +export async function getTriggersType(triggerTypeSlug: string): Promise> { + const url = new URL(`${BASE_URL}/triggers_types/${triggerTypeSlug}`); + return composioApiCall(ZTriggerType, url.toString()); } \ No newline at end of file diff --git a/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts b/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts index 1bc60e72..a65fd18e 100644 --- a/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts +++ b/apps/rowboat/src/application/repositories/composio-trigger-deployments.repository.interface.ts @@ -14,6 +14,7 @@ export const CreateDeploymentSchema = ComposioTriggerDeployment toolkitSlug: true, logo: true, triggerTypeSlug: true, + triggerTypeName: true, triggerConfig: true, }); diff --git a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts index 57c804ee..fdd3bbfc 100644 --- a/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts +++ b/apps/rowboat/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case.ts @@ -2,18 +2,20 @@ import { BadRequestError, NotFoundError } from '@/src/entities/errors/common'; import { z } from "zod"; import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface'; import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy'; -import { CreateDeploymentSchema, IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface'; +import { IComposioTriggerDeploymentsRepository } from '../../repositories/composio-trigger-deployments.repository.interface'; import { IProjectsRepository } from '../../repositories/projects.repository.interface'; -import { composio, getToolkit } from '../../lib/composio/composio'; +import { composio, getTriggersType } from '../../lib/composio/composio'; import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; const inputSchema = z.object({ caller: z.enum(["user", "api"]), userId: z.string().optional(), apiKey: z.string().optional(), - data: CreateDeploymentSchema.omit({ - triggerId: true, - logo: true, + projectId: z.string(), + data: ComposioTriggerDeployment.pick({ + triggerTypeSlug: true, + connectedAccountId: true, + triggerConfig: true, }), }); @@ -46,7 +48,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr async execute(request: z.infer): Promise> { // extract projectid from conversation - const { projectId } = request.data; + const { projectId } = request; // authz check await this.projectActionAuthorizationPolicy.authorize({ @@ -59,8 +61,11 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr // assert and consume quota await this.usageQuotaPolicy.assertAndConsume(projectId); + // get trigger type info + const triggerType = await getTriggersType(request.data.triggerTypeSlug); + // get toolkit info - const toolkit = await getToolkit(request.data.toolkitSlug); + const toolkit = triggerType.toolkit; // ensure that connected account exists on project const project = await this.projectsRepository.fetch(projectId); @@ -69,7 +74,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr } // ensure connected account exists - const account = project.composioConnectedAccounts?.[request.data.toolkitSlug]; + const account = project.composioConnectedAccounts?.[toolkit.slug]; if (!account || account.id !== request.data.connectedAccountId) { throw new BadRequestError('Invalid connected account'); } @@ -81,7 +86,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr } // create trigger on composio - const result = await composio.triggers.create(request.data.projectId, request.data.triggerTypeSlug, { + const result = await composio.triggers.create(projectId, request.data.triggerTypeSlug, { connectedAccountId: request.data.connectedAccountId, triggerConfig: request.data.triggerConfig, }); @@ -89,11 +94,12 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr // create trigger deployment in db return await this.composioTriggerDeploymentsRepository.create({ projectId, - toolkitSlug: request.data.toolkitSlug, - logo: toolkit.meta.logo, + toolkitSlug: toolkit.slug, + logo: toolkit.logo, triggerId: result.triggerId, connectedAccountId: request.data.connectedAccountId, triggerTypeSlug: request.data.triggerTypeSlug, + triggerTypeName: triggerType.name, triggerConfig: request.data.triggerConfig, }); } diff --git a/apps/rowboat/src/entities/models/composio-trigger-deployment.ts b/apps/rowboat/src/entities/models/composio-trigger-deployment.ts index b6ccc294..194b6842 100644 --- a/apps/rowboat/src/entities/models/composio-trigger-deployment.ts +++ b/apps/rowboat/src/entities/models/composio-trigger-deployment.ts @@ -6,6 +6,7 @@ export const ComposioTriggerDeployment = z.object({ triggerId: z.string(), toolkitSlug: z.string(), triggerTypeSlug: z.string(), + triggerTypeName: z.string(), connectedAccountId: z.string(), triggerConfig: z.record(z.string(), z.unknown()), logo: z.string(), diff --git a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts index 275fdb32..d8e4c450 100644 --- a/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts +++ b/apps/rowboat/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller.ts @@ -2,15 +2,16 @@ import { BadRequestError } from "@/src/entities/errors/common"; import z from "zod"; import { ICreateComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/create-composio-trigger-deployment.use-case"; import { ComposioTriggerDeployment } from "@/src/entities/models/composio-trigger-deployment"; -import { CreateDeploymentSchema } from "@/src/application/repositories/composio-trigger-deployments.repository.interface"; const inputSchema = z.object({ caller: z.enum(["user", "api"]), userId: z.string().optional(), apiKey: z.string().optional(), - data: CreateDeploymentSchema.omit({ - triggerId: true, - logo: true, + projectId: z.string(), + data: ComposioTriggerDeployment.pick({ + triggerTypeSlug: true, + connectedAccountId: true, + triggerConfig: true, }), }); @@ -35,13 +36,14 @@ export class CreateComposioTriggerDeploymentController implements ICreateComposi if (!result.success) { throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); } - const { caller, userId, apiKey, data } = result.data; + const { caller, userId, apiKey, projectId, data } = result.data; // execute use case return await this.createComposioTriggerDeploymentUseCase.execute({ caller, userId, apiKey, + projectId, data, }); } From 05fb6ad9a8b68fd7588743acb7ca038329b44c29 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:35:05 +0530 Subject: [PATCH 08/10] update py-sdk docs --- apps/python-sdk/README.md | 137 ++++++++++++--------------------- apps/python-sdk/pyproject.toml | 2 +- 2 files changed, 51 insertions(+), 88 deletions(-) diff --git a/apps/python-sdk/README.md b/apps/python-sdk/README.md index 79f2afeb..53d0a47b 100644 --- a/apps/python-sdk/README.md +++ b/apps/python-sdk/README.md @@ -12,102 +12,65 @@ pip install rowboat ## Usage -### Basic Usage with StatefulChat +### Basic Usage -The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically: - -```python -from rowboat import Client, StatefulChat - -# Initialize the client -client = Client( - host="", - project_id="", - api_key="" -) - -# Create a stateful chat session -chat = StatefulChat(client) - -# Have a conversation -response = chat.run("What is the capital of France?") -print(response) -# The capital of France is Paris. - -# Continue the conversation - the context is maintained automatically -response = chat.run("What other major cities are in that country?") -print(response) -# Other major cities in France include Lyon, Marseille, Toulouse, and Nice. - -response = chat.run("What's the population of the first city you mentioned?") -print(response) -# Lyon has a population of approximately 513,000 in the city proper. -``` - -### Advanced Usage - -#### Using a specific workflow - -You can specify a workflow ID to use a particular conversation configuration: - -```python -chat = StatefulChat( - client, - workflow_id="" -) -``` - -#### Using a test profile - -You can specify a test profile ID to use a specific test configuration: - -```python -chat = StatefulChat( - client, - test_profile_id="" -) -``` - -#### Tool overrides - -You can provide tool override instructions to test a specific configuration: - -```python -chat = StatefulChat( - client, - mock_tools={ - "weather_lookup": "The weather in any city is sunny and 25°C.", - "calculator": "The result of any calculation is 42.", - "search": "Search results for any query return 'No relevant information found.'" - } -) -``` - -### Low-Level Usage - -For more control over the conversation, you can use the `Client` class directly: +The main way to interact with Rowboat is using the `Client` class, which provides a stateless chat API. You can manage conversation state using the `conversationId` returned in each response. ```python +from rowboat.client import Client from rowboat.schema import UserMessage # Initialize the client client = Client( host="", - project_id="", - api_key="" + projectId="", + apiKey="" ) -# Create messages -messages = [ - UserMessage(role='user', content="Hello, how are you?") -] +# Start a new conversation +result = client.run_turn( + messages=[ + UserMessage(role='user', content="list my github repos") + ] +) +print(result.turn.output[-1].content) +print("Conversation ID:", result.conversationId) -# Get response -response = client.chat(messages=messages) -print(response.messages[-1].content) - -# For subsequent messages, you need to manage the message history and state manually -messages.extend(response.messages) -messages.append(UserMessage(role='user', content="What's your name?")) -response = client.chat(messages=messages, state=response.state) +# Continue the conversation by passing the conversationId +result = client.run_turn( + messages=[ + UserMessage(role='user', content="how many did you find?") + ], + conversationId=result.conversationId +) +print(result.turn.output[-1].content) ``` + +### Using Tool Overrides (Mock Tools) + +You can provide tool override instructions to test a specific configuration using the `mockTools` argument: + +```python +result = client.run_turn( + messages=[ + UserMessage(role='user', content="What's the weather?") + ], + mockTools={ + "weather_lookup": "The weather in any city is sunny and 25°C.", + "calculator": "The result of any calculation is 42." + } +) +print(result.turn.output[-1].content) +``` + +### Message Types + +You can use different message types as defined in `rowboat.schema`, such as `UserMessage`, `SystemMessage`, etc. See `schema.py` for all available message types. + +### Error Handling + +If the API returns a non-200 status code, a `ValueError` will be raised with the error details. + +--- + +For more advanced usage, see the docstrings in `client.py` and the message schemas in `schema.py`. diff --git a/apps/python-sdk/pyproject.toml b/apps/python-sdk/pyproject.toml index 7a550f8e..1eb3fd9f 100644 --- a/apps/python-sdk/pyproject.toml +++ b/apps/python-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "rowboat" -version = "5.0.0" +version = "5.0.1" authors = [ { name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" }, ] From ce37f5e9ff52d98ffc233167b7b7bce1a725b0d3 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:14:09 +0530 Subject: [PATCH 09/10] fix credits display on billing page --- apps/rowboat/app/billing/app.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/rowboat/app/billing/app.tsx b/apps/rowboat/app/billing/app.tsx index ee2b6e76..02da8665 100644 --- a/apps/rowboat/app/billing/app.tsx +++ b/apps/rowboat/app/billing/app.tsx @@ -53,6 +53,11 @@ export function BillingPage({ customer, usage }: BillingPageProps) { const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); const [upgradeError, setUpgradeError] = useState(""); + // show friendly values for credits + const sanctionedCredits = Math.floor(usage.sanctionedCredits / (10 ** 6)); + const availableCredits = Math.floor(usage.availableCredits / (10 ** 6)); + const usedCredits = Math.ceil((usage.sanctionedCredits - usage.availableCredits) / (10 ** 6)); + // Prepare usage metrics data const usageData = Object.entries(usage.usage) .map(([type, credits]) => ({ @@ -162,7 +167,7 @@ export function BillingPage({ customer, usage }: BillingPageProps) { tokens.colors.light.text.primary, tokens.colors.dark.text.primary )}> - {usage.sanctionedCredits.toLocaleString()} + {sanctionedCredits.toLocaleString()}

- {(usage.sanctionedCredits - usage.availableCredits).toLocaleString()} + {usedCredits.toLocaleString()}

- {usage.availableCredits.toLocaleString()} + {availableCredits.toLocaleString()}

- Usage data + Usage split
@@ -287,13 +292,13 @@ export function BillingPage({ customer, usage }: BillingPageProps) {
Date: Tue, 19 Aug 2025 14:34:26 +0530 Subject: [PATCH 10/10] update billing modal --- .../components/common/billing-upgrade-modal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/rowboat/components/common/billing-upgrade-modal.tsx b/apps/rowboat/components/common/billing-upgrade-modal.tsx index 18c4a208..fe087517 100644 --- a/apps/rowboat/components/common/billing-upgrade-modal.tsx +++ b/apps/rowboat/components/common/billing-upgrade-modal.tsx @@ -74,17 +74,18 @@ export function BillingUpgradeModal({ isOpen, onClose, errorMessage }: BillingUp plan: "starter" as const, description: "Great for your personal projects", features: [ - "1000 playground chat requests", - "500 copilot requests" + "2,000 credits", + "Latest models like gpt-5, claude-4 and others", ] }, { name: "Pro", plan: "pro" as const, - description: "Great for enterprise teams", + description: "Great for power users or teams", features: [ - "10000 playground chat requests", - "2000 copilot requests" + "20,000 credits", + "o3 and o3-pro", + "Priority support", ], recommended: true }