mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 18:06:30 +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';
|
||||
|
||||
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 { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
|
||||
import { CopyButton } from "../../../../components/common/copy-button";
|
||||
|
|
@ -14,8 +15,7 @@ import { RelativeTime } from "@primer/react";
|
|||
import { Label } from "../../../lib/components/label";
|
||||
import { FormSection } from "../../../lib/components/form-section";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { ProjectSection } from './components/project';
|
||||
import { VoiceSection } from "./components/voice";
|
||||
import { ProjectSection, SimpleProjectSection } from './components/project';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Project config",
|
||||
|
|
@ -187,11 +187,11 @@ export function ApiKeysSection({
|
|||
API keys are used to authenticate requests to the Rowboat API.
|
||||
</p>
|
||||
<Button
|
||||
onPress={handleCreateKey}
|
||||
onClick={handleCreateKey}
|
||||
size="sm"
|
||||
startContent={<Plus className="h-4 w-4" />}
|
||||
variant="flat"
|
||||
isDisabled={loading}
|
||||
variant="primary"
|
||||
disabled={loading}
|
||||
>
|
||||
Create API key
|
||||
</Button>
|
||||
|
|
@ -321,10 +321,10 @@ export function SecretSection({
|
|||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
variant="primary"
|
||||
color="warning"
|
||||
onPress={handleRotateSecret}
|
||||
isDisabled={loading}
|
||||
onClick={handleRotateSecret}
|
||||
disabled={loading}
|
||||
>
|
||||
Rotate
|
||||
</Button>
|
||||
|
|
@ -477,9 +477,8 @@ export function DeleteProjectSection({
|
|||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onPress={onOpen}
|
||||
isDisabled={loading}
|
||||
isLoading={loading}
|
||||
onClick={onOpen}
|
||||
disabled={loading}
|
||||
>
|
||||
Delete project
|
||||
</Button>
|
||||
|
|
@ -508,13 +507,13 @@ export function DeleteProjectSection({
|
|||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={handleDelete}
|
||||
isDisabled={!isValid}
|
||||
onClick={handleDelete}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Delete Project
|
||||
</Button>
|
||||
|
|
@ -566,39 +565,34 @@ export function ConfigApp({
|
|||
useChatWidget: boolean;
|
||||
chatWidgetHost: string;
|
||||
}) {
|
||||
const [selected, setSelected] = useState("general");
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ProjectSection
|
||||
projectId={projectId}
|
||||
useChatWidget={useChatWidget}
|
||||
chatWidgetHost={chatWidgetHost}
|
||||
/>
|
||||
</Panel>
|
||||
</Tab>
|
||||
<Panel title="Project settings">
|
||||
<ProjectSection
|
||||
projectId={projectId}
|
||||
useChatWidget={useChatWidget}
|
||||
chatWidgetHost={chatWidgetHost}
|
||||
/>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<Tab
|
||||
key="twilio"
|
||||
title="Twilio"
|
||||
>
|
||||
<Panel title="Twilio settings">
|
||||
<VoiceSection
|
||||
projectId={projectId}
|
||||
/>
|
||||
</Panel>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
export function SimpleConfigApp({
|
||||
projectId,
|
||||
onProjectConfigUpdated,
|
||||
}: {
|
||||
projectId: string;
|
||||
onProjectConfigUpdated?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<Panel title="Project Settings">
|
||||
<SimpleProjectSection
|
||||
projectId={projectId}
|
||||
onProjectConfigUpdated={onProjectConfigUpdated}
|
||||
/>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
import { ReactNode, useEffect, useState, useCallback } from "react";
|
||||
import { Spinner, Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure } from "@heroui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getProjectConfig, updateProjectName, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../../actions/project_actions";
|
||||
import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName } from "../../../../actions/project_actions";
|
||||
import { CopyButton } from "../../../../../components/common/copy-button";
|
||||
import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import { WithStringId } from "../../../../lib/types/types";
|
||||
|
|
@ -14,6 +13,7 @@ import { RelativeTime } from "@primer/react";
|
|||
import { Label } from "../../../../lib/components/label";
|
||||
import { sectionHeaderStyles, sectionDescriptionStyles } from './shared-styles';
|
||||
import { clsx } from "clsx";
|
||||
import { InputField } from "../../../../lib/components/input-field";
|
||||
|
||||
export function Section({
|
||||
title,
|
||||
|
|
@ -61,10 +61,15 @@ export function RightContent({
|
|||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
function ProjectNameSection({ projectId }: { projectId: string }) {
|
||||
function ProjectNameSection({
|
||||
projectId,
|
||||
onProjectConfigUpdated
|
||||
}: {
|
||||
projectId: string;
|
||||
onProjectConfigUpdated?: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projectName, setProjectName] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
|
|
@ -74,44 +79,32 @@ function ProjectNameSection({ projectId }: { projectId: string }) {
|
|||
});
|
||||
}, [projectId]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setProjectName(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
setError("Project name cannot be empty");
|
||||
return;
|
||||
async function updateName(name: string) {
|
||||
setLoading(true);
|
||||
await updateProjectName(projectId, name);
|
||||
setProjectName(name);
|
||||
setLoading(false);
|
||||
if (onProjectConfigUpdated) {
|
||||
onProjectConfigUpdated();
|
||||
}
|
||||
|
||||
setError(null);
|
||||
updateProjectName(projectId, value);
|
||||
};
|
||||
}
|
||||
|
||||
return <Section
|
||||
title="Project Name"
|
||||
description="The name of your project."
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className={clsx(
|
||||
"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 || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter project name..."
|
||||
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 className="space-y-4">
|
||||
{loading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<InputField
|
||||
type="text"
|
||||
value={projectName || ''}
|
||||
onChange={updateName}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Section>;
|
||||
}
|
||||
|
||||
|
|
@ -361,7 +354,7 @@ function ApiKeysSection({ projectId }: { projectId: string }) {
|
|||
</Section>;
|
||||
}
|
||||
|
||||
function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) {
|
||||
export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [chatClientId, setChatClientId] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -534,11 +527,24 @@ export function ProjectSection({
|
|||
}) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<ProjectNameSection projectId={projectId} />
|
||||
<ProjectIdSection projectId={projectId} />
|
||||
<SecretSection projectId={projectId} />
|
||||
<ApiKeysSection projectId={projectId} />
|
||||
{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} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ export function VoiceSection({ projectId }: { projectId: string }) {
|
|||
>
|
||||
{existingConfig ? 'Update Twilio Config' : 'Import from Twilio'}
|
||||
</Button>
|
||||
{existingConfig ? (
|
||||
{existingConfig && (
|
||||
<Button
|
||||
variant="primary"
|
||||
color="red"
|
||||
|
|
@ -312,24 +312,6 @@ export function VoiceSection({ projectId }: { projectId: string }) {
|
|||
>
|
||||
Delete Configuration
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setFormState({
|
||||
phone: '',
|
||||
accountSid: '',
|
||||
authToken: '',
|
||||
label: ''
|
||||
});
|
||||
setError(null);
|
||||
setIsDirty(false);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Metadata } from "next";
|
||||
import App from "./app";
|
||||
import { USE_CHAT_WIDGET } from "@/app/lib/feature_flags";
|
||||
import { SimpleConfigApp } from "./app";
|
||||
import { requireActiveBillingSubscription } from '@/app/lib/billing';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Project config",
|
||||
title: "Project Settings",
|
||||
};
|
||||
|
||||
export default async function Page(
|
||||
|
|
@ -16,9 +15,7 @@ export default async function Page(
|
|||
) {
|
||||
const params = await props.params;
|
||||
await requireActiveBillingSubscription();
|
||||
return <App
|
||||
return <SimpleConfigApp
|
||||
projectId={params.projectId}
|
||||
useChatWidget={USE_CHAT_WIDGET}
|
||||
chatWidgetHost={process.env.CHAT_WIDGET_HOST || ''}
|
||||
/>;
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ export function App({
|
|||
useRagS3Uploads,
|
||||
useRagScraping,
|
||||
defaultModel,
|
||||
chatWidgetHost,
|
||||
}: {
|
||||
projectId: string;
|
||||
useRag: boolean;
|
||||
|
|
@ -27,6 +28,7 @@ export function App({
|
|||
useRagS3Uploads: boolean;
|
||||
useRagScraping: boolean;
|
||||
defaultModel: string;
|
||||
chatWidgetHost: string;
|
||||
}) {
|
||||
const [mode, setMode] = useState<'draft' | 'live'>('draft');
|
||||
const [project, setProject] = useState<WithStringId<z.infer<typeof Project>> | null>(null);
|
||||
|
|
@ -84,6 +86,13 @@ export function App({
|
|||
setDataSources(updatedDataSources);
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
if (!dataSources) return;
|
||||
|
|
@ -144,6 +153,8 @@ export function App({
|
|||
onRevertToLive={handleRevertToLive}
|
||||
onProjectToolsUpdated={handleProjectToolsUpdate}
|
||||
onDataSourcesUpdated={handleDataSourcesUpdate}
|
||||
onProjectConfigUpdated={handleProjectConfigUpdate}
|
||||
chatWidgetHost={chatWidgetHost}
|
||||
/>}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default async function Page(
|
|||
useRagS3Uploads={USE_RAG_S3_UPLOADS}
|
||||
useRagScraping={USE_RAG_SCRAPING}
|
||||
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 { DataSourceConfig } from "../entities/datasource_config";
|
||||
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 {
|
||||
ResizableHandle,
|
||||
|
|
@ -23,14 +23,19 @@ import {
|
|||
import { Copilot } from "../copilot/app";
|
||||
import { publishWorkflow } 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 { 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 { ProductTour } from "@/components/common/product-tour";
|
||||
import { ModelsResponse } from "@/app/lib/types/billing_types";
|
||||
import { AgentGraphVisualizer } from "../entities/AgentGraphVisualizer";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
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();
|
||||
|
||||
|
|
@ -599,6 +604,8 @@ export function WorkflowEditor({
|
|||
onRevertToLive,
|
||||
onProjectToolsUpdated,
|
||||
onDataSourcesUpdated,
|
||||
onProjectConfigUpdated,
|
||||
chatWidgetHost,
|
||||
}: {
|
||||
projectId: string;
|
||||
dataSources: WithStringId<z.infer<typeof DataSource>>[];
|
||||
|
|
@ -616,6 +623,8 @@ export function WorkflowEditor({
|
|||
onRevertToLive: () => void;
|
||||
onProjectToolsUpdated?: () => void;
|
||||
onDataSourcesUpdated?: () => void;
|
||||
onProjectConfigUpdated?: () => void;
|
||||
chatWidgetHost: string;
|
||||
}) {
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
|
|
@ -652,6 +661,19 @@ export function WorkflowEditor({
|
|||
|
||||
// Modal state for revert confirmation
|
||||
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
|
||||
// useEffect(() => {
|
||||
|
|
@ -877,10 +899,39 @@ export function WorkflowEditor({
|
|||
}
|
||||
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
|
||||
|
||||
// Sync project name when projectConfig changes
|
||||
useEffect(() => {
|
||||
setLocalProjectName(projectConfig.name || '');
|
||||
}, [projectConfig.name]);
|
||||
|
||||
function handlePlaygroundClick() {
|
||||
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 (
|
||||
<EntitySelectionContext.Provider value={{
|
||||
onSelectAgent: handleSelectAgent,
|
||||
|
|
@ -890,6 +941,20 @@ export function WorkflowEditor({
|
|||
<div className="flex flex-col h-full relative">
|
||||
<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">
|
||||
{/* 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} />
|
||||
<div className="flex items-center gap-2">
|
||||
{state.present.publishing && <Spinner size="sm" />}
|
||||
|
|
@ -979,16 +1044,52 @@ export function WorkflowEditor({
|
|||
>
|
||||
<RedoIcon size={16} />
|
||||
</button>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={handlePublishWorkflow}
|
||||
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm"
|
||||
startContent={<RocketIcon size={16} />}
|
||||
data-tour-target="deploy"
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
onPress={handlePublishWorkflow}
|
||||
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"
|
||||
>
|
||||
Deploy
|
||||
</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
|
||||
variant="solid"
|
||||
size="md"
|
||||
|
|
@ -1195,6 +1296,66 @@ export function WorkflowEditor({
|
|||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</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>
|
||||
</EntitySelectionContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
import { usePathname } from "next/navigation";
|
||||
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";
|
||||
|
||||
interface NavLinkProps {
|
||||
|
|
@ -51,13 +51,6 @@ export default function Menu({
|
|||
icon={PlayIcon}
|
||||
selected={pathname.startsWith(`/projects/${projectId}/test`)}
|
||||
/>
|
||||
<NavLink
|
||||
href={`/projects/${projectId}/config`}
|
||||
label="Settings"
|
||||
collapsed={collapsed}
|
||||
icon={SettingsIcon}
|
||||
selected={pathname.startsWith(`/projects/${projectId}/config`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue