mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 18:06:30 +02:00
commit
7f04dc56bd
33 changed files with 425 additions and 354 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>© 2025 RowBoat Labs</div>
|
<div>© 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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
@ -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,32 +152,42 @@ 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">
|
<Button
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
|
size="sm"
|
||||||
{triggerType.description}
|
variant="flat"
|
||||||
</p>
|
|
||||||
<div className="mt-3 flex justify-end">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={() => handleTriggerTypeSelect(triggerType)}
|
onPress={() => handleTriggerTypeSelect(triggerType)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import logoOnly from '@/public/logo-only.png';
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
import { Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
|
||||||
import { UserButton } from "@/app/lib/components/user_button";
|
import { UserButton } from "@/app/lib/components/user_button";
|
||||||
import {
|
import {
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
WorkflowIcon,
|
WorkflowIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
|
@ -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 (
|
{collapsed && <Tooltip
|
||||||
<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>
|
||||||
|
|
@ -247,7 +258,7 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
||||||
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
|
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
|
||||||
{USE_PRODUCT_TOUR && !isProjectsRoute && (
|
{USE_PRODUCT_TOUR && !isProjectsRoute && (
|
||||||
<Tooltip content={collapsed ? "Help" : ""} showArrow placement="right">
|
<Tooltip content={collapsed ? "Help" : ""} showArrow placement="right">
|
||||||
<button
|
<button
|
||||||
onClick={showHelpModal}
|
onClick={showHelpModal}
|
||||||
className={`
|
className={`
|
||||||
w-full rounded-md flex items-center
|
w-full rounded-md flex items-center
|
||||||
|
|
@ -266,7 +277,7 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
||||||
|
|
||||||
{SHOW_DARK_MODE_TOGGLE && (
|
{SHOW_DARK_MODE_TOGGLE && (
|
||||||
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
|
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
className={`
|
className={`
|
||||||
w-full rounded-md flex items-center
|
w-full rounded-md flex items-center
|
||||||
|
|
@ -276,35 +287,26 @@ 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>
|
||||||
|
|
||||||
|
|
||||||
{/* Create Assistant Modal */}
|
{/* Create Assistant Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={handleCreateModalClose}
|
onClose={handleCreateModalClose}
|
||||||
size="2xl"
|
size="2xl"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'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'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"> </div>
|
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue