Merge pull request #213 from rowboatlabs/dev

dev changes
This commit is contained in:
Ramnique Singh 2025-08-20 00:27:07 +05:30 committed by GitHub
commit 7f04dc56bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 425 additions and 354 deletions

View file

@ -25,6 +25,9 @@
<a href="https://www.linkedin.com/company/rowboat-labs" target="_blank" rel="noopener"> <a href="https://www.linkedin.com/company/rowboat-labs" target="_blank" rel="noopener">
<img alt="LinkedIn" src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff"> <img alt="LinkedIn" src="https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff">
</a> </a>
<a href="https://x.com/intent/user?screen_name=rowboatlabshq" target="_blank" rel="noopener">
<img alt="Twitter" src="https://img.shields.io/twitter/follow/rowboatlabshq?style=social">
</a>
<a href="https://www.ycombinator.com" target="_blank" rel="noopener"> <a href="https://www.ycombinator.com" target="_blank" rel="noopener">
<img alt="Y Combinator" src="https://img.shields.io/badge/Y%20Combinator-S24-orange"> <img alt="Y Combinator" src="https://img.shields.io/badge/Y%20Combinator-S24-orange">
</a> </a>

View file

@ -4,6 +4,8 @@ import { redisClient } from "@/app/lib/redis";
import { CopilotAPIRequest } from "@/src/application/lib/copilot/types"; import { CopilotAPIRequest } from "@/src/application/lib/copilot/types";
import { streamMultiAgentResponse } from "@/src/application/lib/copilot/copilot"; import { streamMultiAgentResponse } from "@/src/application/lib/copilot/copilot";
export const maxDuration = 300;
export async function GET(request: Request, props: { params: Promise<{ streamId: string }> }) { export async function GET(request: Request, props: { params: Promise<{ streamId: string }> }) {
const params = await props.params; const params = await props.params;
// get the payload from redis // get the payload from redis

View file

@ -4,6 +4,8 @@ import { requireAuth } from "@/app/lib/auth";
import { z } from "zod"; import { z } from "zod";
import { TurnEvent } from "@/src/entities/models/turn"; import { TurnEvent } from "@/src/entities/models/turn";
export const maxDuration = 300;
export async function GET(request: Request, props: { params: Promise<{ streamId: string }> }) { export async function GET(request: Request, props: { params: Promise<{ streamId: string }> }) {
const params = await props.params; const params = await props.params;

View file

@ -39,10 +39,6 @@ export function App() {
{/* Footer */} {/* Footer */}
<div className="flex flex-col items-center gap-2 text-xs text-white/70"> <div className="flex flex-col items-center gap-2 text-xs text-white/70">
<div>&copy; 2025 RowBoat Labs</div> <div>&copy; 2025 RowBoat Labs</div>
<div className="flex gap-4">
<a className="hover:text-white transition-colors" href="https://www.rowboatlabs.com/terms-of-service" target="_blank" rel="noopener noreferrer">Terms of Service</a>
<a className="hover:text-white transition-colors" href="https://www.rowboatlabs.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
</div>
</div> </div>
</div> </div>
); );

View file

@ -41,6 +41,7 @@ interface TextInputFieldProps extends BaseInputFieldProps {
showSaveButton?: boolean; showSaveButton?: boolean;
showDiscardButton?: boolean; showDiscardButton?: boolean;
immediateSave?: boolean; immediateSave?: boolean;
minHeight?: string;
} }
// Select input specific props // Select input specific props
@ -109,6 +110,7 @@ function TextInputField({
showSaveButton = false, showSaveButton = false,
showDiscardButton = false, showDiscardButton = false,
immediateSave = false, immediateSave = false,
minHeight,
}: TextInputFieldProps) { }: TextInputFieldProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value); const [localValue, setLocalValue] = useState(value);
@ -248,10 +250,10 @@ function TextInputField({
{/* Input field */} {/* Input field */}
{mentions ? ( {mentions ? (
<div className="w-full min-h-[300px]"> <div className="w-full" style={minHeight ? { minHeight } : { minHeight: '300px' }}>
<MentionsEditor <MentionsEditor
atValues={mentionsAtValues} atValues={mentionsAtValues}
value={value} value={localValue}
placeholder={placeholder} placeholder={placeholder}
onValueChange={setLocalValue} onValueChange={setLocalValue}
autoFocus autoFocus
@ -326,10 +328,11 @@ function TextInputField({
"cursor-pointer hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800": !locked && !disabled, "cursor-pointer hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800": !locked && !disabled,
"cursor-not-allowed opacity-60": locked || disabled, "cursor-not-allowed opacity-60": locked || disabled,
"border-0 bg-transparent p-0": inline, "border-0 bg-transparent p-0": inline,
"min-h-[300px]": multiline, "min-h-[300px]": multiline && !minHeight,
"min-h-[40px]": !multiline, "min-h-[40px]": !multiline && !minHeight,
} }
)} )}
style={minHeight ? { minHeight } : undefined}
onClick={() => !locked && !disabled && setIsEditing(true)} onClick={() => !locked && !disabled && setIsEditing(true)}
> >
{/* Content */} {/* Content */}
@ -337,14 +340,14 @@ function TextInputField({
"whitespace-pre-wrap": multiline, "whitespace-pre-wrap": multiline,
"flex items-center": !multiline, "flex items-center": !multiline,
})}> })}>
{value ? ( {(mentions ? localValue : value) ? (
<> <>
{markdown ? ( {markdown ? (
<div className={clsx("prose prose-sm max-w-none", { <div className={clsx("prose prose-sm max-w-none", {
"max-h-[420px] overflow-y-auto": multiline "max-h-[420px] overflow-y-auto": multiline
})}> })}>
<MarkdownContent <MarkdownContent
content={value} content={mentions ? localValue : value}
atValues={mentionsAtValues} atValues={mentionsAtValues}
onMentionNavigate={handleMentionNavigate} onMentionNavigate={handleMentionNavigate}
/> />
@ -355,7 +358,7 @@ function TextInputField({
"max-h-[420px] overflow-y-auto": multiline "max-h-[420px] overflow-y-auto": multiline
})}> })}>
<MarkdownContent <MarkdownContent
content={value} content={mentions ? localValue : value}
atValues={mentionsAtValues} atValues={mentionsAtValues}
onMentionNavigate={handleMentionNavigate} onMentionNavigate={handleMentionNavigate}
/> />

View file

@ -97,13 +97,20 @@ export default function MentionEditor({
}) { }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<Quill | null>(null); const quillRef = useRef<Quill | null>(null);
const atValuesRef = useRef<Match[]>(atValues);
const onValueChangeRef = useRef<typeof onValueChange>(onValueChange);
const externalValueRef = useRef<string>(value);
const isApplyingExternalRef = useRef<boolean>(false);
function getMarkdown(): string { function getMarkdown(): string {
if (!quillRef.current) { if (!quillRef.current) {
return ""; return "";
} }
// generate markdown representation of content // generate markdown representation of content
const markdown = quillRef.current.getContents().map((op) => { const delta = quillRef.current.getContents() as unknown as Delta;
// Quill Delta has .ops
const ops: any[] = (delta as any).ops || [];
const markdown = ops.map((op) => {
if (op.insert && typeof op.insert === 'object' && 'mention' in op.insert) { if (op.insert && typeof op.insert === 'object' && 'mention' in op.insert) {
const mentionOp = op.insert as { mention: Match }; const mentionOp = op.insert as { mention: Match };
return `[@${mentionOp.mention.id}](#mention)`; return `[@${mentionOp.mention.id}](#mention)`;
@ -120,6 +127,20 @@ export default function MentionEditor({
navigator.clipboard.writeText(getMarkdown()); navigator.clipboard.writeText(getMarkdown());
} }
// Keep refs up to date without re-initializing Quill
useEffect(() => {
atValuesRef.current = atValues;
}, [atValues]);
useEffect(() => {
onValueChangeRef.current = onValueChange;
}, [onValueChange]);
useEffect(() => {
externalValueRef.current = value;
}, [value]);
// Initialize Quill once
useEffect(() => { useEffect(() => {
if (!containerRef.current) { if (!containerRef.current) {
return; return;
@ -140,15 +161,14 @@ export default function MentionEditor({
mentionDenotationChars: ["@"], mentionDenotationChars: ["@"],
showDenotationChar: true, showDenotationChar: true,
source: async function (searchTerm: string, renderList: (values: Match[], searchTerm: string) => void) { source: async function (searchTerm: string, renderList: (values: Match[], searchTerm: string) => void) {
const list = atValuesRef.current || [];
if (searchTerm.length === 0) { if (searchTerm.length === 0) {
renderList(atValues, searchTerm); renderList(list, searchTerm);
} else { } else {
const matches = []; const matches: Match[] = [];
for (let i = 0; i < atValues.length; i++) { for (let i = 0; i < list.length; i++) {
if ( if (list[i].value.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1) {
atValues[i].value.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1 matches.push(list[i]);
) {
matches.push(atValues[i]);
} }
} }
renderList(matches, searchTerm); renderList(matches, searchTerm);
@ -165,15 +185,18 @@ export default function MentionEditor({
}); });
// clear the quill contents // clear the quill contents
quill.setContents([]); quill.setText('', Quill.sources.SILENT);
// convert the markdown to parts // convert the markdown to parts
const parts = markdownToParts(value, atValues); const parts = markdownToParts(externalValueRef.current, atValuesRef.current);
insertPartsIntoQuill(quill, parts); insertPartsIntoQuill(quill, parts);
quill.on(Quill.events.TEXT_CHANGE, (delta: Delta, oldDelta: Delta, source: string) => { quill.on(Quill.events.TEXT_CHANGE, (delta: Delta, oldDelta: Delta, source: string) => {
if (onValueChange) { if (isApplyingExternalRef.current) {
onValueChange(getMarkdown()); return;
}
if (onValueChangeRef.current) {
onValueChangeRef.current(getMarkdown());
} }
}); });
quillRef.current = quill; quillRef.current = quill;
@ -193,7 +216,22 @@ export default function MentionEditor({
quillRef.current.off(Quill.events.TEXT_CHANGE); quillRef.current.off(Quill.events.TEXT_CHANGE);
} }
} }
}, [atValues, onValueChange, placeholder, value, autoFocus]); // Mount once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Sync external value into the editor without re-initializing
useEffect(() => {
if (!quillRef.current) return;
const current = getMarkdown();
if (value === current) return;
const quill = quillRef.current;
isApplyingExternalRef.current = true;
quill.setText('', Quill.sources.SILENT);
const parts = markdownToParts(value, atValuesRef.current);
insertPartsIntoQuill(quill, parts);
isApplyingExternalRef.current = false;
}, [value]);
return <div className="relative"> return <div className="relative">
<button className="absolute top-2 right-2 z-10"> <button className="absolute top-2 right-2 z-10">

View file

@ -2,24 +2,29 @@
import { useUser } from '@auth0/nextjs-auth0'; import { useUser } from '@auth0/nextjs-auth0';
import { Avatar, Dropdown, DropdownItem, DropdownSection, DropdownTrigger, DropdownMenu } from "@heroui/react"; import { Avatar, Dropdown, DropdownItem, DropdownSection, DropdownTrigger, DropdownMenu } from "@heroui/react";
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link';
export function UserButton({ useBilling }: { useBilling?: boolean }) { export function UserButton({ useBilling, collapsed }: { useBilling?: boolean, collapsed?: boolean }) {
const router = useRouter(); const router = useRouter();
const { user } = useUser(); const { user } = useUser();
if (!user) { if (!user) {
return <></>; return <></>;
} }
const title = user.email ?? user.name ?? 'Unknown user';
const name = user.name ?? user.email ?? 'Unknown user'; const name = user.name ?? user.email ?? 'Unknown user';
return <Dropdown> return <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Avatar <div className="flex items-center gap-2">
name={name} <Avatar
size="sm" name={name}
className="cursor-pointer" size='md'
/> isBordered
radius='md'
className='shrink-0'
/>
{!collapsed && <span className="text-sm truncate">{name}</span>}
</div>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
onAction={(key) => { onAction={(key) => {
@ -31,7 +36,7 @@ export function UserButton({ useBilling }: { useBilling?: boolean }) {
} }
}} }}
> >
<DropdownSection title={name}> <DropdownSection title={title}>
{useBilling ? ( {useBilling ? (
<DropdownItem key="billing"> <DropdownItem key="billing">
Billing Billing

View file

@ -818,7 +818,9 @@ export function SimpleProjectSection({
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<ProjectNameSection projectId={projectId} onProjectConfigUpdated={onProjectConfigUpdated} /> <ProjectNameSection projectId={projectId} onProjectConfigUpdated={onProjectConfigUpdated} />
<ProjectIdSection projectId={projectId} />
<SecretSection projectId={projectId} /> <SecretSection projectId={projectId} />
<ApiKeysSection projectId={projectId} />
<DisconnectToolkitsSection projectId={projectId} onProjectConfigUpdated={onProjectConfigUpdated} /> <DisconnectToolkitsSection projectId={projectId} onProjectConfigUpdated={onProjectConfigUpdated} />
<DeleteProjectSection projectId={projectId} /> <DeleteProjectSection projectId={projectId} />
</div> </div>

View file

@ -319,6 +319,22 @@ export function AgentConfig({
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{/* Description Section */}
<div className="space-y-2">
<label className={sectionHeaderStyles}>Description</label>
<InputField
type="text"
value={agent.description || ""}
onChange={(value: string) => {
handleUpdate({ ...agent, description: value });
showSavedMessage();
}}
multiline={true}
placeholder="Enter a description for this agent"
minHeight="40px"
className="w-full"
/>
</div>
{/* Instructions Section */} {/* Instructions Section */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -493,22 +509,7 @@ export function AgentConfig({
/> />
</div> </div>
</div> </div>
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
<div className="flex-1">
<InputField
type="text"
value={agent.description || ""}
onChange={(value: string) => {
handleUpdate({ ...agent, description: value });
showSavedMessage();
}}
multiline={true}
placeholder="Enter a description for this agent"
className="w-full"
/>
</div>
</div>
</div> </div>
</SectionCard> </SectionCard>
{/* Behavior Section Card */} {/* Behavior Section Card */}

View file

@ -55,7 +55,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string
'Deployment ID': reason.triggerDeploymentId, 'Deployment ID': reason.triggerDeploymentId,
}, },
payload: reason.payload, payload: reason.payload,
link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null link: reason.triggerDeploymentId ? `/projects/${projectId}/manage-triggers/triggers/${reason.triggerDeploymentId}` : null
}; };
} }
if (reason.type === 'scheduled_job_rule') { if (reason.type === 'scheduled_job_rule') {
@ -65,7 +65,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string
'Rule ID': reason.ruleId, 'Rule ID': reason.ruleId,
}, },
payload: null, payload: null,
link: `/projects/${projectId}/job-rules/scheduled/${reason.ruleId}` link: `/projects/${projectId}/manage-triggers/scheduled/${reason.ruleId}`
}; };
} }
if (reason.type === 'recurring_job_rule') { if (reason.type === 'recurring_job_rule') {
@ -75,7 +75,7 @@ export function JobView({ projectId, jobId }: { projectId: string; jobId: string
'Rule ID': reason.ruleId, 'Rule ID': reason.ruleId,
}, },
payload: null, payload: null,
link: `/projects/${projectId}/job-rules/recurring/${reason.ruleId}` link: `/projects/${projectId}/manage-triggers/recurring/${reason.ruleId}`
}; };
} }
return { return {

View file

@ -99,21 +99,21 @@ export function JobsList({ projectId, filters, showTitle = true, customTitle }:
return { return {
type: 'Composio Trigger', type: 'Composio Trigger',
display: `Composio: ${reason.triggerTypeSlug}`, display: `Composio: ${reason.triggerTypeSlug}`,
link: reason.triggerDeploymentId ? `/projects/${projectId}/job-rules/triggers/${reason.triggerDeploymentId}` : null link: reason.triggerDeploymentId ? `/projects/${projectId}/manage-triggers/triggers/${reason.triggerDeploymentId}` : null
}; };
} }
if (reason.type === 'scheduled_job_rule') { if (reason.type === 'scheduled_job_rule') {
return { return {
type: 'Scheduled Job Rule', type: 'Scheduled Job Rule',
display: `Scheduled Rule`, display: `Scheduled Rule`,
link: `/projects/${projectId}/job-rules/scheduled/${reason.ruleId}` link: `/projects/${projectId}/manage-triggers/scheduled/${reason.ruleId}`
}; };
} }
if (reason.type === 'recurring_job_rule') { if (reason.type === 'recurring_job_rule') {
return { return {
type: 'Recurring Job Rule', type: 'Recurring Job Rule',
display: `Recurring Rule`, display: `Recurring Rule`,
link: `/projects/${projectId}/job-rules/recurring/${reason.ruleId}` link: `/projects/${projectId}/manage-triggers/recurring/${reason.ruleId}`
}; };
} }
return { return {

View file

@ -47,7 +47,7 @@ export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { pro
setDeleting(true); setDeleting(true);
try { try {
await deleteComposioTriggerDeployment({ projectId, deploymentId: deployment.id }); await deleteComposioTriggerDeployment({ projectId, deploymentId: deployment.id });
window.location.href = `/projects/${projectId}/job-rules`; window.location.href = `/projects/${projectId}/manage-triggers?tab=triggers`;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert('Failed to delete trigger'); alert('Failed to delete trigger');
@ -62,9 +62,8 @@ export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { pro
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers?tab=triggers`}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
</Link> </Link>
@ -77,9 +76,9 @@ export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { pro
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
<Trash2Icon className="w-4 h-4" />
Delete Delete
</Button> </Button>
</div> </div>
@ -151,14 +150,15 @@ export function ComposioTriggerDeploymentView({ projectId, deploymentId }: { pro
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Delete External Trigger</h3> <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Delete External Trigger</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.</p> <p className="text-sm text-gray-600 dark:text-gray-400 mb-6">Are you sure you want to delete this external trigger? This will remove the linked webhook in Composio and delete this deployment.</p>
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<Button variant="secondary" onClick={() => setShowDeleteConfirm(false)} disabled={deleting}>Cancel</Button> <Button variant="secondary" onClick={() => setShowDeleteConfirm(false)} disabled={deleting} className="whitespace-nowrap">Cancel</Button>
<Button <Button
variant="secondary" variant="secondary"
onClick={handleDelete} onClick={handleDelete}
disabled={deleting} isLoading={deleting}
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
{deleting ? (<><Spinner size="sm" /> Deleting...</>) : (<><Trash2Icon className="w-4 h-4" /> Delete</>)} {deleting ? 'Deleting...' : 'Delete'}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -89,7 +89,7 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
input: { messages: convertedMessages }, input: { messages: convertedMessages },
cron: cronExpression, cron: cronExpression,
}); });
router.push(`/projects/${projectId}/job-rules`); router.push(`/projects/${projectId}/manage-triggers?tab=recurring`);
} catch (error) { } catch (error) {
console.error("Failed to create recurring job rule:", error); console.error("Failed to create recurring job rule:", error);
alert("Failed to create recurring job rule"); alert("Failed to create recurring job rule");
@ -102,9 +102,8 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers?tab=recurring`}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
</Link> </Link>
@ -181,9 +180,9 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
onClick={addMessage} onClick={addMessage}
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex items-center gap-2" startContent={<PlusIcon className="w-4 h-4" />}
className="whitespace-nowrap"
> >
<PlusIcon className="w-4 h-4" />
Add Message Add Message
</Button> </Button>
</div> </div>
@ -231,7 +230,8 @@ export function CreateRecurringJobRuleForm({ projectId }: { projectId: string })
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="px-6 py-2" isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
> >
{loading ? "Creating..." : "Create Rule"} {loading ? "Creating..." : "Create Rule"}
</Button> </Button>

View file

@ -1,18 +1,35 @@
'use client'; 'use client';
import { useState } from "react"; import { useEffect, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Tabs, Tab } from "@/components/ui/tabs"; import { Tabs, Tab } from "@/components/ui/tabs";
import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list"; import { ScheduledJobRulesList } from "../scheduled/components/scheduled-job-rules-list";
import { RecurringJobRulesList } from "./recurring-job-rules-list"; import { RecurringJobRulesList } from "./recurring-job-rules-list";
import { TriggersTab } from "./triggers-tab"; import { TriggersTab } from "./triggers-tab";
export function JobRulesTabs({ projectId }: { projectId: string }) { export function JobRulesTabs({ projectId }: { projectId: string }) {
const [activeTab, setActiveTab] = useState<string>("triggers"); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const initialTab = (searchParams.get('tab') ?? 'triggers');
const [activeTab, setActiveTab] = useState<string>(initialTab);
const handleTabChange = (key: React.Key) => { const handleTabChange = (key: React.Key) => {
setActiveTab(key.toString()); const nextTab = key.toString();
setActiveTab(nextTab);
const params = new URLSearchParams(searchParams.toString());
params.set('tab', nextTab);
router.replace(`${pathname}?${params.toString()}`);
}; };
useEffect(() => {
const current = searchParams.get('tab') ?? 'triggers';
if (current !== activeTab) {
setActiveTab(current);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<Tabs <Tabs

View file

@ -65,7 +65,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
ruleId: rule.id, ruleId: rule.id,
}); });
// Redirect back to job rules list // Redirect back to job rules list
router.push(`/projects/${projectId}/job-rules`); router.push(`/projects/${projectId}/manage-triggers?tab=recurring`);
} catch (error) { } catch (error) {
console.error("Failed to delete rule:", error); console.error("Failed to delete rule:", error);
alert("Failed to delete rule"); alert("Failed to delete rule");
@ -118,7 +118,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
<Panel title="Rule Not Found"> <Panel title="Rule Not Found">
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400">The requested rule could not be found.</p> <p className="text-gray-500 dark:text-gray-400">The requested rule could not be found.</p>
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers`}>
<Button variant="secondary" className="mt-4"> <Button variant="secondary" className="mt-4">
Back to Job Rules Back to Job Rules
</Button> </Button>
@ -133,9 +133,8 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers?tab=recurring`}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
</Link> </Link>
@ -149,31 +148,21 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
<Button <Button
onClick={handleToggleStatus} onClick={handleToggleStatus}
disabled={updating} disabled={updating}
variant={rule.disabled ? "secondary" : "primary"} variant={rule.disabled ? "primary" : "secondary"}
size="sm" size="sm"
className="flex items-center gap-2" isLoading={updating}
startContent={rule.disabled ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
className="whitespace-nowrap"
> >
{updating ? ( {rule.disabled ? 'Activate' : 'Pause'}
<Spinner size="sm" />
) : rule.disabled ? (
<>
<PlayIcon className="w-4 h-4" />
Enable
</>
) : (
<>
<PauseIcon className="w-4 h-4" />
Disable
</>
)}
</Button> </Button>
<Button <Button
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
<Trash2Icon className="w-4 h-4" />
Delete Delete
</Button> </Button>
</div> </div>
@ -297,6 +286,7 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary" variant="secondary"
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
disabled={deleting} disabled={deleting}
className="whitespace-nowrap"
> >
Cancel Cancel
</Button> </Button>
@ -304,19 +294,11 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary" variant="secondary"
onClick={handleDelete} onClick={handleDelete}
disabled={deleting} disabled={deleting}
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" isLoading={deleting}
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
{deleting ? ( {deleting ? 'Deleting...' : 'Delete'}
<>
<Spinner size="sm" />
Deleting...
</>
) : (
<>
<Trash2Icon className="w-4 h-4" />
Delete
</>
)}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -134,7 +134,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
} }
rightActions={ rightActions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules/recurring/new`}> <Link href={`/projects/${projectId}/manage-triggers/recurring/new`}>
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}> <Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
New Recurring Trigger New Recurring Trigger
</Button> </Button>
@ -168,7 +168,7 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<Link <Link
href={`/projects/${projectId}/job-rules/recurring/${item.id}`} href={`/projects/${projectId}/manage-triggers/recurring/${item.id}`}
className="block" className="block"
> >
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
@ -220,15 +220,10 @@ export function RecurringJobRulesList({ projectId }: { projectId: string }) {
disabled={loadingMore} disabled={loadingMore}
variant="secondary" variant="secondary"
size="sm" size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
> >
{loadingMore ? ( {loadingMore ? 'Loading...' : 'Load More'}
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
</Button> </Button>
</div> </div>
)} )}

View file

@ -4,12 +4,13 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Spinner, Link } from '@heroui/react'; import { Spinner, Link } from '@heroui/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Panel } from '@/components/common/panel-common'; import { Panel } from '@/components/common/panel-common';
import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp } from 'lucide-react'; import { Plus, Trash2, ZapIcon, ChevronDown, ChevronUp, ArrowLeftIcon } from 'lucide-react';
import Image from 'next/image';
import { z } from 'zod'; import { z } from 'zod';
import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment'; import { ComposioTriggerDeployment } from '@/src/entities/models/composio-trigger-deployment';
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date'; import { isToday, isThisWeek, isThisMonth } from '@/lib/utils/date';
import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment, listComposioTriggerTypes } from '@/app/actions/composio.actions'; import { listComposioTriggerDeployments, deleteComposioTriggerDeployment, createComposioTriggerDeployment } from '@/app/actions/composio.actions';
import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit'; import { SelectComposioToolkit } from '../../tools/components/SelectComposioToolkit';
import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel'; import { ComposioTriggerTypesPanel } from '../../workflow/components/ComposioTriggerTypesPanel';
import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm'; import { TriggerConfigForm } from '../../workflow/components/TriggerConfigForm';
@ -20,6 +21,8 @@ import { fetchProject } from '@/app/actions/project.actions';
type TriggerDeployment = z.infer<typeof ComposioTriggerDeployment>; type TriggerDeployment = z.infer<typeof ComposioTriggerDeployment>;
// Removed friendly name computation; backend now provides friendly trigger name
export function TriggersTab({ projectId }: { projectId: string }) { export function TriggersTab({ projectId }: { projectId: string }) {
const [triggers, setTriggers] = useState<TriggerDeployment[]>([]); const [triggers, setTriggers] = useState<TriggerDeployment[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -31,7 +34,6 @@ export function TriggersTab({ projectId }: { projectId: string }) {
const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false); const [isSubmittingTrigger, setIsSubmittingTrigger] = useState(false);
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null); const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null); const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
const [triggerTypeNames, setTriggerTypeNames] = useState<Record<string, string>>({});
const [expandedTrigger, setExpandedTrigger] = useState<string | null>(null); const [expandedTrigger, setExpandedTrigger] = useState<string | null>(null);
const [cursor, setCursor] = useState<string | null>(null); const [cursor, setCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState<boolean>(false); const [hasMore, setHasMore] = useState<boolean>(false);
@ -46,31 +48,6 @@ export function TriggersTab({ projectId }: { projectId: string }) {
} }
}, [projectId]); }, [projectId]);
const loadTriggerTypeNames = useCallback(async () => {
try {
const names: Record<string, string> = {};
// 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 sections = useMemo(() => {
const groups: Record<string, TriggerDeployment[]> = { const groups: Record<string, TriggerDeployment[]> = {
Today: [], Today: [],
@ -204,7 +181,11 @@ export function TriggersTab({ projectId }: { projectId: string }) {
triggerConfig, triggerConfig,
}); });
// Success! Go back to triggers list and reload // Success! Go back to triggers list tab and reload
if (typeof window !== 'undefined') {
window.location.href = `/projects/${projectId}/manage-triggers?tab=triggers`;
return;
}
handleBackToList(); handleBackToList();
} catch (err: any) { } catch (err: any) {
console.error('Error creating trigger:', err); console.error('Error creating trigger:', err);
@ -225,10 +206,8 @@ export function TriggersTab({ projectId }: { projectId: string }) {
}, [showCreateFlow, loadTriggers]); }, [showCreateFlow, loadTriggers]);
useEffect(() => { useEffect(() => {
if (triggers.length > 0) { // No-op: trigger names are now derived from slug locally
loadTriggerTypeNames(); }, [triggers]);
}
}, [triggers, loadTriggerTypeNames]);
const renderTriggerList = () => { const renderTriggerList = () => {
if (loading) { if (loading) {
@ -261,7 +240,7 @@ export function TriggersTab({ projectId }: { projectId: string }) {
</div> </div>
} }
rightActions={ rightActions={
<Button variant="secondary" onClick={loadTriggers}> <Button variant="secondary" onClick={loadTriggers} className="whitespace-nowrap">
Try Again Try Again
</Button> </Button>
} }
@ -349,13 +328,29 @@ export function TriggersTab({ projectId }: { projectId: string }) {
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<a href={`/projects/${projectId}/job-rules/triggers/${trigger.id}`} className="block"> <a href={`/projects/${projectId}/manage-triggers/triggers/${trigger.id}`} className="block">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-1">
<span className="text-sm font-medium text-green-600 dark:text-green-400"> {trigger.logo && (
Active <Image
</span> src={trigger.logo}
alt={`${trigger.toolkitSlug} logo`}
width={20}
height={20}
className="rounded"
unoptimized
/>
)}
{trigger.toolkitSlug && (
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-900 text-gray-600 dark:text-gray-400">
{trigger.toolkitSlug}
</span>
)}
</div>
<div className="h-2" />
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-medium text-green-600 dark:text-green-400">Active</span>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"> <span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{triggerTypeNames[trigger.triggerTypeSlug] || trigger.triggerTypeSlug} {trigger.triggerTypeName}
</span> </span>
</div> </div>
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
@ -373,10 +368,9 @@ export function TriggersTab({ projectId }: { projectId: string }) {
size="sm" size="sm"
isLoading={deletingTrigger === trigger.id} isLoading={deletingTrigger === trigger.id}
onClick={() => handleDeleteTrigger(trigger.id)} onClick={() => handleDeleteTrigger(trigger.id)}
startContent={<Trash2 className="w-4 h-4" />}
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950" className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-950"
> />
<Trash2 className="w-4 h-4" />
</Button>
</div> </div>
{/* Advanced Details Section - Collapsible */} {/* Advanced Details Section - Collapsible */}
@ -422,15 +416,10 @@ export function TriggersTab({ projectId }: { projectId: string }) {
disabled={loadingMore} disabled={loadingMore}
variant="secondary" variant="secondary"
size="sm" size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
> >
{loadingMore ? ( {loadingMore ? 'Loading...' : 'Load More'}
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
</Button> </Button>
</div> </div>
)} )}
@ -471,8 +460,10 @@ export function TriggersTab({ projectId }: { projectId: string }) {
<Button <Button
variant="secondary" variant="secondary"
onClick={handleBackToList} onClick={handleBackToList}
startContent={<ArrowLeftIcon className="w-4 h-4" />}
className="whitespace-nowrap"
> >
Back to Triggers Back to Triggers
</Button> </Button>
</div> </div>

View file

@ -90,7 +90,7 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
input: { messages: convertedMessages }, input: { messages: convertedMessages },
scheduledTime: scheduledTimeString, scheduledTime: scheduledTimeString,
}); });
router.push(`/projects/${projectId}/job-rules`); router.push(`/projects/${projectId}/manage-triggers?tab=scheduled`);
} catch (error) { } catch (error) {
console.error("Failed to create scheduled job rule:", error); console.error("Failed to create scheduled job rule:", error);
alert("Failed to create scheduled job rule"); alert("Failed to create scheduled job rule");
@ -105,9 +105,8 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers?tab=scheduled`}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
</Link> </Link>
@ -147,9 +146,9 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
onClick={addMessage} onClick={addMessage}
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex items-center gap-2" startContent={<PlusIcon className="w-4 h-4" />}
className="whitespace-nowrap"
> >
<PlusIcon className="w-4 h-4" />
Add Message Add Message
</Button> </Button>
</div> </div>
@ -197,7 +196,8 @@ export function CreateScheduledJobRuleForm({ projectId }: { projectId: string })
<Button <Button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="px-6 py-2" isLoading={loading}
className="px-6 py-2 whitespace-nowrap"
> >
{loading ? "Creating..." : "Create Rule"} {loading ? "Creating..." : "Create Rule"}
</Button> </Button>

View file

@ -46,7 +46,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
ruleId: rule.id, ruleId: rule.id,
}); });
// Redirect back to job rules list // Redirect back to job rules list
router.push(`/projects/${projectId}/job-rules`); router.push(`/projects/${projectId}/manage-triggers?tab=scheduled`);
} catch (error) { } catch (error) {
console.error("Failed to delete rule:", error); console.error("Failed to delete rule:", error);
alert("Failed to delete rule"); alert("Failed to delete rule");
@ -80,9 +80,8 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
<Panel <Panel
title={ title={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules`}> <Link href={`/projects/${projectId}/manage-triggers?tab=scheduled`}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm" startContent={<ArrowLeftIcon className="w-4 h-4" />} className="whitespace-nowrap">
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back Back
</Button> </Button>
</Link> </Link>
@ -97,9 +96,9 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
variant="secondary" variant="secondary"
size="sm" size="sm"
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
<Trash2Icon className="w-4 h-4" />
Delete Delete
</Button> </Button>
</div> </div>
@ -204,6 +203,7 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary" variant="secondary"
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
disabled={deleting} disabled={deleting}
className="whitespace-nowrap"
> >
Cancel Cancel
</Button> </Button>
@ -211,19 +211,11 @@ export function ScheduledJobRuleView({ projectId, ruleId }: { projectId: string;
variant="secondary" variant="secondary"
onClick={handleDelete} onClick={handleDelete}
disabled={deleting} disabled={deleting}
className="flex items-center gap-2 bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800" isLoading={deleting}
startContent={<Trash2Icon className="w-4 h-4" />}
className="bg-red-50 hover:bg-red-100 text-red-700 dark:bg-red-950 dark:hover:bg-red-900 dark:text-red-400 border border-red-200 dark:border-red-800 whitespace-nowrap"
> >
{deleting ? ( {deleting ? 'Deleting...' : 'Delete'}
<>
<Spinner size="sm" />
Deleting...
</>
) : (
<>
<Trash2Icon className="w-4 h-4" />
Delete
</>
)}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -112,7 +112,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
} }
rightActions={ rightActions={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href={`/projects/${projectId}/job-rules/scheduled/new`}> <Link href={`/projects/${projectId}/manage-triggers/scheduled/new`}>
<Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}> <Button size="sm" className="whitespace-nowrap" startContent={<PlusIcon className="w-4 h-4" />}>
New One-time Trigger New One-time Trigger
</Button> </Button>
@ -146,7 +146,7 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<Link <Link
href={`/projects/${projectId}/job-rules/scheduled/${item.id}`} href={`/projects/${projectId}/manage-triggers/scheduled/${item.id}`}
className="block" className="block"
> >
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
@ -190,15 +190,10 @@ export function ScheduledJobRulesList({ projectId }: { projectId: string }) {
disabled={loadingMore} disabled={loadingMore}
variant="secondary" variant="secondary"
size="sm" size="sm"
isLoading={loadingMore}
className="whitespace-nowrap"
> >
{loadingMore ? ( {loadingMore ? 'Loading...' : 'Load More'}
<>
<Spinner size="sm" />
Loading...
</>
) : (
'Load More'
)}
</Button> </Button>
</div> </div>
)} )}

View file

@ -1,12 +1,13 @@
'use client'; 'use client';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Button, Card, CardBody, CardHeader, Spinner } from '@heroui/react'; import { Button, Card, CardBody, Spinner } from '@heroui/react';
import { ChevronLeft, ChevronRight, ZapIcon, ArrowLeft } from 'lucide-react'; import { ChevronLeft, ChevronRight, ZapIcon, ArrowLeft } from 'lucide-react';
import { z } from 'zod'; import { z } from 'zod';
import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type'; import { ComposioTriggerType } from '@/src/entities/models/composio-trigger-type';
import { listComposioTriggerTypes } from '@/app/actions/composio.actions'; import { listComposioTriggerTypes } from '@/app/actions/composio.actions';
import { ZToolkit } from "@/src/application/lib/composio/types"; import { ZToolkit } from "@/src/application/lib/composio/types";
import { PictureImg } from '@/components/ui/picture-img';
interface ComposioTriggerTypesPanelProps { interface ComposioTriggerTypesPanelProps {
toolkit: z.infer<typeof ZToolkit>; toolkit: z.infer<typeof ZToolkit>;
@ -151,29 +152,39 @@ export function ComposioTriggerTypesPanel({
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3">
{triggerTypes.map((triggerType) => ( {triggerTypes.map((triggerType) => (
<Card <Card
key={triggerType.slug} key={triggerType.slug}
className="cursor-pointer hover:shadow-md transition-shadow" className="group p-6 rounded-xl transition-all duration-200 cursor-pointer bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 shadow-md dark:shadow-gray-900/20 hover:shadow-lg dark:hover:shadow-gray-900/30 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-gray-50/50 hover:-translate-y-1 min-h-[200px] flex flex-col"
isPressable isPressable
onPress={() => handleTriggerTypeSelect(triggerType)} onPress={() => handleTriggerTypeSelect(triggerType)}
> >
<CardHeader className="flex gap-3"> <div className="flex items-start gap-3 mb-2">
<div className="flex items-center justify-center w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg"> {toolkit.meta?.logo ? (
<ZapIcon className="w-6 h-6 text-blue-600 dark:text-blue-400" /> <PictureImg
</div> src={toolkit.meta.logo}
<div className="flex flex-col"> alt={`${toolkit.name} logo`}
<p className="text-base font-semibold text-gray-900 dark:text-gray-100"> className="w-8 h-8 rounded-md object-cover flex-shrink-0"
/>
) : (
<div className="flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-md">
<ZapIcon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate text-left">
{triggerType.name} {triggerType.name}
</h3>
</div>
</div>
<CardBody className="pt-0 px-0 flex-1 flex flex-col">
<div className="flex-1">
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
{triggerType.description}
</p> </p>
</div> </div>
</CardHeader> <div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700 flex justify-end">
<CardBody className="pt-0">
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
{triggerType.description}
</p>
<div className="mt-3 flex justify-end">
<Button <Button
size="sm" size="sm"
variant="flat" variant="flat"

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react"; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Input } from "@heroui/react";
import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock } from "lucide-react"; import { RadioIcon, RedoIcon, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, SettingsIcon, ChevronDownIcon, ZapIcon, Clock, Plug } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
interface TopBarProps { interface TopBarProps {
@ -22,7 +22,6 @@ interface TopBarProps {
onChangeMode: (mode: 'draft' | 'live') => void; onChangeMode: (mode: 'draft' | 'live') => void;
onRevertToLive: () => void; onRevertToLive: () => void;
onToggleCopilot: () => void; onToggleCopilot: () => void;
onSettingsModalOpen: () => void;
} }
export function TopBar({ export function TopBar({
@ -43,7 +42,6 @@ export function TopBar({
onChangeMode, onChangeMode,
onRevertToLive, onRevertToLive,
onToggleCopilot, onToggleCopilot,
onSettingsModalOpen,
}: TopBarProps) { }: TopBarProps) {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
@ -107,7 +105,7 @@ export function TopBar({
<div className="text-green-500">Copied to clipboard</div> <div className="text-green-500">Copied to clipboard</div>
</div>} </div>}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isLive && <div className="flex items-center gap-2"> {isLive && <div className="flex items-center gap-2 absolute left-1/2 transform -translate-x-1/2">
<div className="bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2"> <div className="bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
<AlertTriangle size={16} /> <AlertTriangle size={16} />
This version is locked. Changes applied will not be reflected. This version is locked. Changes applied will not be reflected.
@ -135,43 +133,59 @@ export function TopBar({
{/* Deploy CTA - always visible */} {/* Deploy CTA - always visible */}
<div className="flex"> <div className="flex">
<Button {isLive ? (
variant="solid" <Dropdown>
size="md" <DropdownTrigger>
onPress={onPublishWorkflow} <Button
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm rounded-r-none" variant="solid"
startContent={<RocketIcon size={16} />} size="md"
data-tour-target="deploy" className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
> startContent={<Plug size={16} />}
Deploy >
</Button> Use Assistant
<Dropdown> <ChevronDownIcon size={14} />
<DropdownTrigger> </Button>
</DropdownTrigger>
<DropdownMenu aria-label="Assistant access options">
<DropdownItem
key="api-sdk"
startContent={<SettingsIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/config`); } }}
>
API & SDK Settings
</DropdownItem>
<DropdownItem
key="manage-triggers"
startContent={<ZapIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/manage-triggers`); } }}
>
Manage Triggers
</DropdownItem>
</DropdownMenu>
</Dropdown>
) : (
<>
<Button <Button
variant="solid" variant="solid"
size="md" size="md"
className="min-w-0 px-2 bg-green-600 hover:bg-green-700 border-l-1 border-green-500 text-white font-semibold text-sm rounded-l-none" onPress={onPublishWorkflow}
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm rounded-r-none"
startContent={<RocketIcon size={16} />}
data-tour-target="deploy"
> >
<ChevronDownIcon size={14} /> Publish
</Button> </Button>
</DropdownTrigger> <Dropdown>
<DropdownMenu aria-label="Deploy actions"> <DropdownTrigger>
<DropdownItem <Button
key="settings" variant="solid"
startContent={<SettingsIcon size={16} />} size="md"
onPress={onSettingsModalOpen} className="min-w-0 px-2 bg-green-600 hover:bg-green-700 border-l-1 border-green-500 text-white font-semibold text-sm rounded-l-none"
> >
API & SDK settings <ChevronDownIcon size={14} />
</DropdownItem> </Button>
<DropdownItem </DropdownTrigger>
key="manage-triggers" <DropdownMenu aria-label="Deploy actions">
startContent={<ZapIcon size={16} />}
onPress={() => { if (projectId) { router.push(`/projects/${projectId}/job-rules`); } }}
>
Manage triggers
</DropdownItem>
{!isLive ? (
<>
<DropdownItem <DropdownItem
key="view-live" key="view-live"
startContent={<RadioIcon size={16} />} startContent={<RadioIcon size={16} />}
@ -187,10 +201,10 @@ export function TopBar({
> >
Reset to live version Reset to live version
</DropdownItem> </DropdownItem>
</> </DropdownMenu>
) : null} </Dropdown>
</DropdownMenu> </>
</Dropdown> )}
</div> </div>
{isLive && <div className="flex items-center gap-2"> {isLive && <div className="flex items-center gap-2">
@ -199,6 +213,7 @@ export function TopBar({
size="md" size="md"
onPress={() => onChangeMode('draft')} onPress={() => onChangeMode('draft')}
className="gap-2 px-4 bg-gray-600 hover:bg-gray-700 text-white font-semibold text-sm" className="gap-2 px-4 bg-gray-600 hover:bg-gray-700 text-white font-semibold text-sm"
startContent={<PenLine size={16} />}
> >
Switch to draft Switch to draft
</Button> </Button>

View file

@ -199,7 +199,9 @@ export function TriggerConfigForm({
onChange={(e) => handleFieldChange(fieldName, e.target.value)} onChange={(e) => handleFieldChange(fieldName, e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400" focus:outline-none focus:ring-0 focus:ring-transparent focus:ring-offset-0
focus:border-blue-500 dark:focus:border-blue-400
transition-all duration-200"
required={isRequired} required={isRequired}
> >
<option value="">Select {property.title || fieldName}</option> <option value="">Select {property.title || fieldName}</option>
@ -234,6 +236,11 @@ export function TriggerConfigForm({
description={property.description} description={property.description}
isInvalid={!!fieldError} isInvalid={!!fieldError}
errorMessage={fieldError} errorMessage={fieldError}
classNames={{
base: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none",
mainWrapper: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none",
inputWrapper: "ring-0 !ring-0 outline-none !outline-none shadow-none !shadow-none focus:ring-0 !focus:ring-0 focus:ring-transparent !focus:ring-transparent focus-visible:ring-0 !focus-visible:ring-0 focus-visible:outline-none !focus-visible:outline-none focus-within:ring-0 !focus-within:ring-0 focus-within:shadow-none !focus-within:shadow-none data-[focus=true]:ring-0 group-data-[focus=true]:ring-0 data-[focus=true]:shadow-none group-data-[focus=true]:shadow-none",
}}
/> />
); );
})} })}

View file

@ -34,7 +34,7 @@ import { ModelsResponse } from "@/app/lib/types/billing_types";
import { AgentGraphVisualizer } from "../entities/AgentGraphVisualizer"; import { AgentGraphVisualizer } from "../entities/AgentGraphVisualizer";
import { Panel } from "@/components/common/panel-common"; import { Panel } from "@/components/common/panel-common";
import { Button as CustomButton } from "@/components/ui/button"; import { Button as CustomButton } from "@/components/ui/button";
import { ConfigApp } from "../config/app";
import { InputField } from "@/app/lib/components/input-field"; import { InputField } from "@/app/lib/components/input-field";
import { VoiceSection } from "../config/components/voice"; import { VoiceSection } from "../config/components/voice";
import { TopBar } from "./components/TopBar"; import { TopBar } from "./components/TopBar";
@ -376,7 +376,7 @@ function reducer(state: State, action: Action): State {
properties: {}, properties: {},
required: [] required: []
}, },
mockTool: true, mockTool: false,
...action.tool ...action.tool
}); });
draft.selection = { draft.selection = {
@ -872,9 +872,6 @@ export function WorkflowEditor({
// Modal state for revert confirmation // Modal state for revert confirmation
const { isOpen: isRevertModalOpen, onOpen: onRevertModalOpen, onClose: onRevertModalClose } = useDisclosure(); const { isOpen: isRevertModalOpen, onOpen: onRevertModalOpen, onClose: onRevertModalClose } = useDisclosure();
// Modal state for settings
const { isOpen: isSettingsModalOpen, onOpen: onSettingsModalOpen, onClose: onSettingsModalClose } = useDisclosure();
// Modal state for phone/Twilio configuration // Modal state for phone/Twilio configuration
const { isOpen: isPhoneModalOpen, onOpen: onPhoneModalOpen, onClose: onPhoneModalClose } = useDisclosure(); const { isOpen: isPhoneModalOpen, onOpen: onPhoneModalOpen, onClose: onPhoneModalClose } = useDisclosure();
@ -1280,7 +1277,6 @@ export function WorkflowEditor({
onChangeMode={onChangeMode} onChangeMode={onChangeMode}
onRevertToLive={handleRevertToLive} onRevertToLive={handleRevertToLive}
onToggleCopilot={() => setShowCopilot(!showCopilot)} onToggleCopilot={() => setShowCopilot(!showCopilot)}
onSettingsModalOpen={onSettingsModalOpen}
/> />
{/* Content Area */} {/* Content Area */}
@ -1498,26 +1494,7 @@ export function WorkflowEditor({
</ModalContent> </ModalContent>
</Modal> </Modal>
{/* Settings Modal */}
<Modal
isOpen={isSettingsModalOpen}
onClose={onSettingsModalClose}
size="5xl"
scrollBehavior="inside"
>
<ModalContent className="h-[80vh]">
<ModalHeader className="flex flex-col gap-1">
API & SDK
</ModalHeader>
<ModalBody className="p-0">
<ConfigApp
projectId={projectId}
useChatWidget={USE_CHAT_WIDGET}
chatWidgetHost={chatWidgetHost}
/>
</ModalBody>
</ModalContent>
</Modal>
{/* Phone/Twilio Modal */} {/* Phone/Twilio Modal */}
<Modal <Modal

View file

@ -102,31 +102,26 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
href: 'workflow', href: 'workflow',
label: 'Build', label: 'Build',
icon: WorkflowIcon, icon: WorkflowIcon,
requiresProject: true
}, },
{ {
href: 'job-rules', href: 'manage-triggers',
label: 'Triggers', label: 'Triggers',
icon: ZapIcon, icon: ZapIcon,
requiresProject: true
}, },
{ {
href: 'conversations', href: 'conversations',
label: 'Conversations', label: 'Conversations',
icon: MessageSquareIcon, icon: MessageSquareIcon,
requiresProject: true
}, },
{ {
href: 'jobs', href: 'jobs',
label: 'Jobs', label: 'Jobs',
icon: LogsIcon, icon: LogsIcon,
requiresProject: true
}, },
{ {
href: 'config', href: 'config',
label: 'Settings', label: 'Settings',
icon: SettingsIcon, icon: SettingsIcon,
requiresProject: true
} }
]; ];
@ -148,7 +143,7 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
<div className="flex flex-col grow"> <div className="flex flex-col grow">
{/* Rowboat Logo */} {/* Rowboat Logo */}
<div className="p-3 border-b border-zinc-100 dark:border-zinc-800"> <div className="p-3 border-b border-zinc-100 dark:border-zinc-800">
<Tooltip content={collapsed ? "Rowboat" : ""} showArrow placement="right"> <Tooltip content="Home" showArrow placement="right">
<Link <Link
href="/projects" href="/projects"
className={` className={`
@ -159,8 +154,8 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
{collapsed && <Image {collapsed && <Image
src={logoOnly} src={logoOnly}
alt="Rowboat" alt="Rowboat"
width={24} width={32}
height={24} height={32}
/>} />}
{!collapsed && <Image {!collapsed && <Image
src={logo} src={logo}
@ -179,49 +174,65 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
const Icon = item.icon; const Icon = item.icon;
const fullPath = `/projects/${projectId}/${item.href}`; const fullPath = `/projects/${projectId}/${item.href}`;
const isActive = pathname.startsWith(fullPath); const isActive = pathname.startsWith(fullPath);
const isDisabled = isProjectsRoute && item.requiresProject;
return ( return <>
<Tooltip {collapsed && <Tooltip
key={item.href} key={item.href}
content={collapsed ? item.label : ""} content={collapsed ? item.label : ""}
showArrow showArrow
placement="right" placement="right"
> >
<Link <Link
href={isDisabled ? '#' : fullPath} href={fullPath}
className={` className={`
relative w-full rounded-md flex items-center relative w-full rounded-md flex items-center
text-[15px] font-medium transition-all duration-200 text-[15px] font-medium transition-all duration-200
${collapsed ? 'justify-center py-4' : 'px-2.5 py-3 gap-2.5'} px-2.5 py-3 gap-2.5
${isActive ${isActive
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-l-2 border-indigo-600 dark:border-indigo-400' ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-l-2 border-indigo-600 dark:border-indigo-400'
: isDisabled : 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300'
? 'text-zinc-300 dark:text-zinc-600 cursor-not-allowed'
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300'
} }
${isDisabled ? 'pointer-events-none' : ''}
`} `}
data-tour-target={item.href === 'config' ? 'settings' : item.href === 'sources' ? 'entity-data-sources' : undefined} data-tour-target={item.href === 'config' ? 'settings' : item.href === 'sources' ? 'entity-data-sources' : undefined}
> >
<Icon <Icon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE} size={COLLAPSED_ICON_SIZE}
className={` className={`
transition-all duration-200 transition-all duration-200
${isDisabled ${isActive
? 'text-zinc-300 dark:text-zinc-600' ? 'text-indigo-600 dark:text-indigo-400'
: isActive : 'text-zinc-500 dark:text-zinc-400'
? 'text-indigo-600 dark:text-indigo-400'
: 'text-zinc-500 dark:text-zinc-400'
} }
`} `}
/> />
{!collapsed && (
<span>{item.label}</span>
)}
</Link> </Link>
</Tooltip> </Tooltip>}
); {!collapsed && <Link
href={fullPath}
className={`
relative w-full rounded-md flex items-center
text-[15px] font-medium transition-all duration-200
px-2.5 py-3 gap-2.5
${isActive
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-l-2 border-indigo-600 dark:border-indigo-400'
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300'
}
`}
data-tour-target={item.href === 'config' ? 'settings' : item.href === 'sources' ? 'entity-data-sources' : undefined}
>
<Icon
size={EXPANDED_ICON_SIZE}
className={`
transition-all duration-200
${isActive
? 'text-indigo-600 dark:text-indigo-400'
: 'text-zinc-500 dark:text-zinc-400'
}
`}
/>
<span>{item.label}</span>
</Link>}
</>
}) })
)} )}
</nav> </nav>
@ -276,27 +287,18 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
text-zinc-600 dark:text-zinc-400 text-zinc-600 dark:text-zinc-400
`} `}
> >
{ theme == "light" ? <Moon size={COLLAPSED_ICON_SIZE} /> : <Sun size={COLLAPSED_ICON_SIZE} /> } {theme == "light" ? <Moon size={COLLAPSED_ICON_SIZE} /> : <Sun size={COLLAPSED_ICON_SIZE} />}
{!collapsed && <span>Appearance</span>} {!collapsed && <span>Appearance</span>}
</button> </button>
</Tooltip> </Tooltip>
)} )}
{useAuth && ( {useAuth && <>
<Tooltip content={collapsed ? "Account" : ""} showArrow placement="right"> {collapsed && <Tooltip content="Account" showArrow placement="right">
<div <UserButton useBilling={useBilling} collapsed={collapsed} />
className={` </Tooltip>}
w-full rounded-md flex items-center {!collapsed && <UserButton useBilling={useBilling} collapsed={collapsed} />}
text-[15px] font-medium transition-all duration-200 </>}
${collapsed ? 'justify-center py-4' : 'px-4 py-4 gap-3'}
hover:bg-zinc-100 dark:hover:bg-zinc-800/50
`}
>
<UserButton useBilling={useBilling} />
{!collapsed && <span>Account</span>}
</div>
</Tooltip>
)}
</div> </div>
</div> </div>
</aside> </aside>

View file

@ -84,7 +84,6 @@ export function BillingUpgradeModal({ isOpen, onClose, errorMessage }: BillingUp
description: "Great for power users or teams", description: "Great for power users or teams",
features: [ features: [
"20,000 credits", "20,000 credits",
"o3 and o3-pro",
"Priority support", "Priority support",
], ],
recommended: true recommended: true

View file

@ -82,11 +82,47 @@ export function Panel({
data-tour-target={tourTarget} data-tour-target={tourTarget}
> >
{variant === 'copilot' && showWelcome && ( {variant === 'copilot' && showWelcome && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-16"> <div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-8">
{/* Replace Sparkles icon with mascot image */} {/* Replace Sparkles icon with mascot image */}
<Image src={mascot} alt="Rowboat Mascot" width={192} height={192} className="object-contain mb-2 animate-float" /> <Image src={mascot} alt="Rowboat Mascot" width={192} height={192} className="object-contain mb-4 animate-float" />
{/* Welcome/Intro Section */}
<div className="text-center max-w-md px-6 mb-4">
<h3 className="text-xl font-semibold text-zinc-700 dark:text-zinc-300 mb-3 text-center">
👋 Welcome to Rowboat!
</h3>
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-6 text-center">
I&apos;m your copilot for building agents and adding tools to them.
</p>
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4 text-center">
Here&apos;s what you can do in Rowboat:
</p>
<div className="space-y-3 max-w-2xl mx-auto text-left">
<div className="flex items-start gap-3">
<span className="text-lg"></span>
<span className="text-sm text-zinc-600 dark:text-zinc-400">Build AI agents instantly with natural language.</span>
</div>
<div className="flex items-start gap-3">
<span className="text-lg">🔌</span>
<span className="text-sm text-zinc-600 dark:text-zinc-400">Connect tools with one-click integrations.</span>
</div>
<div className="flex items-start gap-3">
<span className="text-lg">📂</span>
<span className="text-sm text-zinc-600 dark:text-zinc-400">Power with knowledge by adding documents for RAG.</span>
</div>
<div className="flex items-start gap-3">
<span className="text-lg">🔄</span>
<span className="text-sm text-zinc-600 dark:text-zinc-400">Automate workflows by setting up triggers and actions.</span>
</div>
<div className="flex items-start gap-3">
<span className="text-lg">🚀</span>
<span className="text-sm text-zinc-600 dark:text-zinc-400">Deploy anywhere via API or SDK.</span>
</div>
</div>
</div>
{SHOW_COPILOT_MARQUEE && ( {SHOW_COPILOT_MARQUEE && (
<div className="relative mt-8 max-w-full px-8"> <div className="relative mt-4 max-w-full px-8">
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex"> <div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div> <div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor">&nbsp;</div> <div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor">&nbsp;</div>