mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-05 05:12:38 +02:00
Deployment settings (#189)
* moved settings to a modal inside the build view under deployment * project name is a editable field in the build view * project name is propogated correctly * remove project name from settings modal * split settings into phone and api * added chat widget option to the deploy settings * bring back top level settings tab with few options * removed cancel option from twilio settings
This commit is contained in:
parent
55b2204912
commit
9d54d971d2
8 changed files with 274 additions and 129 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { Spinner, Textarea, Button, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider, Tab, Tabs } from "@heroui/react";
|
import { Spinner, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider, Textarea } from "@heroui/react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
|
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
|
||||||
import { CopyButton } from "../../../../components/common/copy-button";
|
import { CopyButton } from "../../../../components/common/copy-button";
|
||||||
|
|
@ -14,8 +15,7 @@ import { RelativeTime } from "@primer/react";
|
||||||
import { Label } from "../../../lib/components/label";
|
import { Label } from "../../../lib/components/label";
|
||||||
import { FormSection } from "../../../lib/components/form-section";
|
import { FormSection } from "../../../lib/components/form-section";
|
||||||
import { Panel } from "@/components/common/panel-common";
|
import { Panel } from "@/components/common/panel-common";
|
||||||
import { ProjectSection } from './components/project';
|
import { ProjectSection, SimpleProjectSection } from './components/project';
|
||||||
import { VoiceSection } from "./components/voice";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Project config",
|
title: "Project config",
|
||||||
|
|
@ -187,11 +187,11 @@ export function ApiKeysSection({
|
||||||
API keys are used to authenticate requests to the Rowboat API.
|
API keys are used to authenticate requests to the Rowboat API.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onPress={handleCreateKey}
|
onClick={handleCreateKey}
|
||||||
size="sm"
|
size="sm"
|
||||||
startContent={<Plus className="h-4 w-4" />}
|
startContent={<Plus className="h-4 w-4" />}
|
||||||
variant="flat"
|
variant="primary"
|
||||||
isDisabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Create API key
|
Create API key
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -321,10 +321,10 @@ export function SecretSection({
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="primary"
|
||||||
color="warning"
|
color="warning"
|
||||||
onPress={handleRotateSecret}
|
onClick={handleRotateSecret}
|
||||||
isDisabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Rotate
|
Rotate
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -477,9 +477,8 @@ export function DeleteProjectSection({
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={onOpen}
|
onClick={onOpen}
|
||||||
isDisabled={loading}
|
disabled={loading}
|
||||||
isLoading={loading}
|
|
||||||
>
|
>
|
||||||
Delete project
|
Delete project
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -508,13 +507,13 @@ export function DeleteProjectSection({
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="light" onPress={onClose}>
|
<Button variant="secondary" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
onPress={handleDelete}
|
onClick={handleDelete}
|
||||||
isDisabled={!isValid}
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
Delete Project
|
Delete Project
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -566,19 +565,8 @@ export function ConfigApp({
|
||||||
useChatWidget: boolean;
|
useChatWidget: boolean;
|
||||||
chatWidgetHost: string;
|
chatWidgetHost: string;
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState("general");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto p-6">
|
<div className="h-full overflow-auto p-6">
|
||||||
<Tabs
|
|
||||||
selectedKey={selected}
|
|
||||||
onSelectionChange={(key) => setSelected(key.toString())}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
key="general"
|
|
||||||
title="Project settings"
|
|
||||||
>
|
|
||||||
<Panel title="Project settings">
|
<Panel title="Project settings">
|
||||||
<ProjectSection
|
<ProjectSection
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
|
@ -586,19 +574,25 @@ export function ConfigApp({
|
||||||
chatWidgetHost={chatWidgetHost}
|
chatWidgetHost={chatWidgetHost}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Tab>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<Tab
|
export function SimpleConfigApp({
|
||||||
key="twilio"
|
projectId,
|
||||||
title="Twilio"
|
onProjectConfigUpdated,
|
||||||
>
|
}: {
|
||||||
<Panel title="Twilio settings">
|
projectId: string;
|
||||||
<VoiceSection
|
onProjectConfigUpdated?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-auto p-6">
|
||||||
|
<Panel title="Project Settings">
|
||||||
|
<SimpleProjectSection
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
onProjectConfigUpdated={onProjectConfigUpdated}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
import { ReactNode, useEffect, useState, useCallback } from "react";
|
import { ReactNode, useEffect, useState, useCallback } from "react";
|
||||||
import { Spinner, Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure } from "@heroui/react";
|
import { Spinner, Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure } from "@heroui/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName } from "../../../../actions/project_actions";
|
||||||
import { getProjectConfig, updateProjectName, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../../actions/project_actions";
|
|
||||||
import { CopyButton } from "../../../../../components/common/copy-button";
|
import { CopyButton } from "../../../../../components/common/copy-button";
|
||||||
import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||||
import { WithStringId } from "../../../../lib/types/types";
|
import { WithStringId } from "../../../../lib/types/types";
|
||||||
|
|
@ -14,6 +13,7 @@ import { RelativeTime } from "@primer/react";
|
||||||
import { Label } from "../../../../lib/components/label";
|
import { Label } from "../../../../lib/components/label";
|
||||||
import { sectionHeaderStyles, sectionDescriptionStyles } from './shared-styles';
|
import { sectionHeaderStyles, sectionDescriptionStyles } from './shared-styles';
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
|
import { InputField } from "../../../../lib/components/input-field";
|
||||||
|
|
||||||
export function Section({
|
export function Section({
|
||||||
title,
|
title,
|
||||||
|
|
@ -61,10 +61,15 @@ export function RightContent({
|
||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectNameSection({ projectId }: { projectId: string }) {
|
function ProjectNameSection({
|
||||||
|
projectId,
|
||||||
|
onProjectConfigUpdated
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
onProjectConfigUpdated?: () => void;
|
||||||
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [projectName, setProjectName] = useState<string | null>(null);
|
const [projectName, setProjectName] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -74,44 +79,32 @@ function ProjectNameSection({ projectId }: { projectId: string }) {
|
||||||
});
|
});
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
async function updateName(name: string) {
|
||||||
const value = e.target.value;
|
setLoading(true);
|
||||||
setProjectName(value);
|
await updateProjectName(projectId, name);
|
||||||
|
setProjectName(name);
|
||||||
if (!value.trim()) {
|
setLoading(false);
|
||||||
setError("Project name cannot be empty");
|
if (onProjectConfigUpdated) {
|
||||||
return;
|
onProjectConfigUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(null);
|
|
||||||
updateProjectName(projectId, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Section
|
return <Section
|
||||||
title="Project Name"
|
title="Project Name"
|
||||||
description="The name of your project."
|
description="The name of your project."
|
||||||
>
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Spinner size="sm" />
|
<Spinner size="sm" />
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<InputField
|
||||||
<div className={clsx(
|
type="text"
|
||||||
"border rounded-lg focus-within:ring-2",
|
|
||||||
error
|
|
||||||
? "border-red-500 focus-within:ring-red-500/20"
|
|
||||||
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
|
|
||||||
)}>
|
|
||||||
<Textarea
|
|
||||||
value={projectName || ''}
|
value={projectName || ''}
|
||||||
onChange={handleChange}
|
onChange={updateName}
|
||||||
placeholder="Enter project name..."
|
className="w-full"
|
||||||
className="w-full text-sm bg-transparent border-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
|
|
||||||
autoResize
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</Section>;
|
</Section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,7 +354,7 @@ function ApiKeysSection({ projectId }: { projectId: string }) {
|
||||||
</Section>;
|
</Section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) {
|
export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [chatClientId, setChatClientId] = useState<string | null>(null);
|
const [chatClientId, setChatClientId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -534,11 +527,24 @@ export function ProjectSection({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<ProjectNameSection projectId={projectId} />
|
|
||||||
<ProjectIdSection projectId={projectId} />
|
<ProjectIdSection projectId={projectId} />
|
||||||
<SecretSection projectId={projectId} />
|
|
||||||
<ApiKeysSection projectId={projectId} />
|
<ApiKeysSection projectId={projectId} />
|
||||||
{useChatWidget && <ChatWidgetSection projectId={projectId} chatWidgetHost={chatWidgetHost} />}
|
{useChatWidget && <ChatWidgetSection projectId={projectId} chatWidgetHost={chatWidgetHost} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SimpleProjectSection({
|
||||||
|
projectId,
|
||||||
|
onProjectConfigUpdated,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
onProjectConfigUpdated?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<ProjectNameSection projectId={projectId} onProjectConfigUpdated={onProjectConfigUpdated} />
|
||||||
|
<SecretSection projectId={projectId} />
|
||||||
<DeleteProjectSection projectId={projectId} />
|
<DeleteProjectSection projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ export function VoiceSection({ projectId }: { projectId: string }) {
|
||||||
>
|
>
|
||||||
{existingConfig ? 'Update Twilio Config' : 'Import from Twilio'}
|
{existingConfig ? 'Update Twilio Config' : 'Import from Twilio'}
|
||||||
</Button>
|
</Button>
|
||||||
{existingConfig ? (
|
{existingConfig && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
color="red"
|
color="red"
|
||||||
|
|
@ -312,24 +312,6 @@ export function VoiceSection({ projectId }: { projectId: string }) {
|
||||||
>
|
>
|
||||||
Delete Configuration
|
Delete Configuration
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setFormState({
|
|
||||||
phone: '',
|
|
||||||
accountSid: '',
|
|
||||||
authToken: '',
|
|
||||||
label: ''
|
|
||||||
});
|
|
||||||
setError(null);
|
|
||||||
setIsDirty(false);
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import App from "./app";
|
import { SimpleConfigApp } from "./app";
|
||||||
import { USE_CHAT_WIDGET } from "@/app/lib/feature_flags";
|
|
||||||
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Project config",
|
title: "Project Settings",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Page(
|
export default async function Page(
|
||||||
|
|
@ -16,9 +15,7 @@ export default async function Page(
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
await requireActiveBillingSubscription();
|
await requireActiveBillingSubscription();
|
||||||
return <App
|
return <SimpleConfigApp
|
||||||
projectId={params.projectId}
|
projectId={params.projectId}
|
||||||
useChatWidget={USE_CHAT_WIDGET}
|
|
||||||
chatWidgetHost={process.env.CHAT_WIDGET_HOST || ''}
|
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ export function App({
|
||||||
useRagS3Uploads,
|
useRagS3Uploads,
|
||||||
useRagScraping,
|
useRagScraping,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
|
chatWidgetHost,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
useRag: boolean;
|
useRag: boolean;
|
||||||
|
|
@ -27,6 +28,7 @@ export function App({
|
||||||
useRagS3Uploads: boolean;
|
useRagS3Uploads: boolean;
|
||||||
useRagScraping: boolean;
|
useRagScraping: boolean;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
|
chatWidgetHost: string;
|
||||||
}) {
|
}) {
|
||||||
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
||||||
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
||||||
|
|
@ -84,6 +86,13 @@ export function App({
|
||||||
setDataSources(updatedDataSources);
|
setDataSources(updatedDataSources);
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
|
const handleProjectConfigUpdate = useCallback(async () => {
|
||||||
|
// Refresh project config when project name or other settings change
|
||||||
|
const updatedProjectConfig = await getProjectConfig(projectId);
|
||||||
|
setProject(updatedProjectConfig);
|
||||||
|
setProjectConfig(updatedProjectConfig);
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
// Auto-update data sources when there are pending ones
|
// Auto-update data sources when there are pending ones
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataSources) return;
|
if (!dataSources) return;
|
||||||
|
|
@ -144,6 +153,8 @@ export function App({
|
||||||
onRevertToLive={handleRevertToLive}
|
onRevertToLive={handleRevertToLive}
|
||||||
onProjectToolsUpdated={handleProjectToolsUpdate}
|
onProjectToolsUpdated={handleProjectToolsUpdate}
|
||||||
onDataSourcesUpdated={handleDataSourcesUpdate}
|
onDataSourcesUpdated={handleDataSourcesUpdate}
|
||||||
|
onProjectConfigUpdated={handleProjectConfigUpdate}
|
||||||
|
chatWidgetHost={chatWidgetHost}
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export default async function Page(
|
||||||
useRagS3Uploads={USE_RAG_S3_UPLOADS}
|
useRagS3Uploads={USE_RAG_S3_UPLOADS}
|
||||||
useRagScraping={USE_RAG_SCRAPING}
|
useRagScraping={USE_RAG_SCRAPING}
|
||||||
defaultModel={DEFAULT_MODEL}
|
defaultModel={DEFAULT_MODEL}
|
||||||
|
chatWidgetHost={process.env.CHAT_WIDGET_HOST || ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner,
|
||||||
import { PromptConfig } from "../entities/prompt_config";
|
import { PromptConfig } from "../entities/prompt_config";
|
||||||
import { DataSourceConfig } from "../entities/datasource_config";
|
import { DataSourceConfig } from "../entities/datasource_config";
|
||||||
import { RelativeTime } from "@primer/react";
|
import { RelativeTime } from "@primer/react";
|
||||||
import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags";
|
import { USE_PRODUCT_TOUR, USE_CHAT_WIDGET } from "@/app/lib/feature_flags";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
|
|
@ -23,14 +23,19 @@ import {
|
||||||
import { Copilot } from "../copilot/app";
|
import { Copilot } from "../copilot/app";
|
||||||
import { publishWorkflow } from "@/app/actions/project_actions";
|
import { publishWorkflow } from "@/app/actions/project_actions";
|
||||||
import { saveWorkflow } from "@/app/actions/project_actions";
|
import { saveWorkflow } from "@/app/actions/project_actions";
|
||||||
|
import { updateProjectName } from "@/app/actions/project_actions";
|
||||||
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
||||||
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon } from "lucide-react";
|
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle, DownloadIcon, XIcon, SettingsIcon, ChevronDownIcon, PhoneIcon, MessageCircleIcon } from "lucide-react";
|
||||||
import { EntityList } from "./entity_list";
|
import { EntityList } from "./entity_list";
|
||||||
import { ProductTour } from "@/components/common/product-tour";
|
import { ProductTour } from "@/components/common/product-tour";
|
||||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
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 { VoiceSection } from "../config/components/voice";
|
||||||
|
import { ChatWidgetSection } from "../config/components/project";
|
||||||
|
|
||||||
enablePatches();
|
enablePatches();
|
||||||
|
|
||||||
|
|
@ -599,6 +604,8 @@ export function WorkflowEditor({
|
||||||
onRevertToLive,
|
onRevertToLive,
|
||||||
onProjectToolsUpdated,
|
onProjectToolsUpdated,
|
||||||
onDataSourcesUpdated,
|
onDataSourcesUpdated,
|
||||||
|
onProjectConfigUpdated,
|
||||||
|
chatWidgetHost,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||||
|
|
@ -616,6 +623,8 @@ export function WorkflowEditor({
|
||||||
onRevertToLive: () => void;
|
onRevertToLive: () => void;
|
||||||
onProjectToolsUpdated?: () => void;
|
onProjectToolsUpdated?: () => void;
|
||||||
onDataSourcesUpdated?: () => void;
|
onDataSourcesUpdated?: () => void;
|
||||||
|
onProjectConfigUpdated?: () => void;
|
||||||
|
chatWidgetHost: string;
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
|
@ -653,6 +662,19 @@ 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
|
||||||
|
const { isOpen: isPhoneModalOpen, onOpen: onPhoneModalOpen, onClose: onPhoneModalClose } = useDisclosure();
|
||||||
|
|
||||||
|
// Modal state for chat widget configuration
|
||||||
|
const { isOpen: isChatWidgetModalOpen, onOpen: onChatWidgetModalOpen, onClose: onChatWidgetModalClose } = useDisclosure();
|
||||||
|
|
||||||
|
// Project name state
|
||||||
|
const [localProjectName, setLocalProjectName] = useState<string>(projectConfig.name || '');
|
||||||
|
const [projectNameError, setProjectNameError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load agent order from localStorage on mount
|
// Load agent order from localStorage on mount
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const mode = isLive ? 'live' : 'draft';
|
// const mode = isLive ? 'live' : 'draft';
|
||||||
|
|
@ -877,10 +899,39 @@ export function WorkflowEditor({
|
||||||
}
|
}
|
||||||
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
|
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
|
||||||
|
|
||||||
|
// Sync project name when projectConfig changes
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalProjectName(projectConfig.name || '');
|
||||||
|
}, [projectConfig.name]);
|
||||||
|
|
||||||
function handlePlaygroundClick() {
|
function handlePlaygroundClick() {
|
||||||
setIsInitialState(false);
|
setIsInitialState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateProjectName = (value: string) => {
|
||||||
|
if (value.length === 0) {
|
||||||
|
setProjectNameError("Project name cannot be empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setProjectNameError(null);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProjectNameChange = async (value: string) => {
|
||||||
|
setLocalProjectName(value);
|
||||||
|
|
||||||
|
if (validateProjectName(value)) {
|
||||||
|
try {
|
||||||
|
await updateProjectName(projectId, value);
|
||||||
|
// Trigger refresh of project config to update all references to project name
|
||||||
|
onProjectConfigUpdated?.();
|
||||||
|
} catch (error) {
|
||||||
|
setProjectNameError("Failed to update project name");
|
||||||
|
console.error('Failed to update project name:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntitySelectionContext.Provider value={{
|
<EntitySelectionContext.Provider value={{
|
||||||
onSelectAgent: handleSelectAgent,
|
onSelectAgent: handleSelectAgent,
|
||||||
|
|
@ -890,6 +941,20 @@ export function WorkflowEditor({
|
||||||
<div className="flex flex-col h-full relative">
|
<div className="flex flex-col h-full relative">
|
||||||
<div className="shrink-0 flex justify-between items-center pb-6">
|
<div className="shrink-0 flex justify-between items-center pb-6">
|
||||||
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
||||||
|
{/* Project Name Editor */}
|
||||||
|
<div className="flex flex-col min-w-0 max-w-xs">
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
value={localProjectName}
|
||||||
|
onChange={handleProjectNameChange}
|
||||||
|
error={projectNameError}
|
||||||
|
placeholder="Project name..."
|
||||||
|
className="text-lg font-semibold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-6 w-px bg-gray-300 dark:bg-gray-600"></div>
|
||||||
|
|
||||||
<WorkflowIcon size={16} />
|
<WorkflowIcon size={16} />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{state.present.publishing && <Spinner size="sm" />}
|
{state.present.publishing && <Spinner size="sm" />}
|
||||||
|
|
@ -979,16 +1044,52 @@ export function WorkflowEditor({
|
||||||
>
|
>
|
||||||
<RedoIcon size={16} />
|
<RedoIcon size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
<div className="flex">
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="md"
|
size="md"
|
||||||
onPress={handlePublishWorkflow}
|
onPress={handlePublishWorkflow}
|
||||||
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm"
|
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} />}
|
startContent={<RocketIcon size={16} />}
|
||||||
data-tour-target="deploy"
|
data-tour-target="deploy"
|
||||||
>
|
>
|
||||||
Deploy
|
Deploy
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="Deploy actions">
|
||||||
|
<DropdownItem
|
||||||
|
key="settings"
|
||||||
|
startContent={<SettingsIcon size={16} />}
|
||||||
|
onPress={onSettingsModalOpen}
|
||||||
|
>
|
||||||
|
API & SDK
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="phone"
|
||||||
|
startContent={<PhoneIcon size={16} />}
|
||||||
|
onPress={onPhoneModalOpen}
|
||||||
|
>
|
||||||
|
Phone
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="chat-widget"
|
||||||
|
startContent={<MessageCircleIcon size={16} />}
|
||||||
|
onPress={onChatWidgetModalOpen}
|
||||||
|
>
|
||||||
|
Chat widget
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="md"
|
size="md"
|
||||||
|
|
@ -1195,6 +1296,66 @@ export function WorkflowEditor({
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</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 */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isPhoneModalOpen}
|
||||||
|
onClose={onPhoneModalClose}
|
||||||
|
size="4xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalContent className="h-[80vh]">
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
Phone Configuration
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="p-0">
|
||||||
|
<VoiceSection projectId={projectId} />
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Chat Widget Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isChatWidgetModalOpen}
|
||||||
|
onClose={onChatWidgetModalClose}
|
||||||
|
size="4xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalContent className="h-[70vh]">
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
Chat Widget
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="p-0">
|
||||||
|
<div className="p-6">
|
||||||
|
<ChatWidgetSection
|
||||||
|
projectId={projectId}
|
||||||
|
chatWidgetHost={chatWidgetHost}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</EntitySelectionContext.Provider>
|
</EntitySelectionContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SettingsIcon, WorkflowIcon, PlayIcon, LucideIcon } from "lucide-react";
|
import { WorkflowIcon, PlayIcon, LucideIcon } from "lucide-react";
|
||||||
import MenuItem from "./components/menu-item";
|
import MenuItem from "./components/menu-item";
|
||||||
|
|
||||||
interface NavLinkProps {
|
interface NavLinkProps {
|
||||||
|
|
@ -51,13 +51,6 @@ export default function Menu({
|
||||||
icon={PlayIcon}
|
icon={PlayIcon}
|
||||||
selected={pathname.startsWith(`/projects/${projectId}/test`)}
|
selected={pathname.startsWith(`/projects/${projectId}/test`)}
|
||||||
/>
|
/>
|
||||||
<NavLink
|
|
||||||
href={`/projects/${projectId}/config`}
|
|
||||||
label="Settings"
|
|
||||||
collapsed={collapsed}
|
|
||||||
icon={SettingsIcon}
|
|
||||||
selected={pathname.startsWith(`/projects/${projectId}/config`)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue