mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
fix(webhook-ui): fixed webhook ui (#1301)
* update infra and remove railway
* fix(webhook-ui): fixed webhook ui
* Revert "update infra and remove railway"
This reverts commit 88669ad0b7.
* feat(control-bar): updated export controls and webhook settings
* additional styling improvements to chat deploy & templates modals
* fix test event
---------
Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
@@ -124,17 +124,15 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
if (webhook.includeRateLimits) {
|
||||
;(payload.data as any).rateLimits = {
|
||||
workflowExecutionRateLimit: {
|
||||
sync: {
|
||||
limit: 60,
|
||||
remaining: 45,
|
||||
resetAt: new Date(timestamp + 60000).toISOString(),
|
||||
},
|
||||
async: {
|
||||
limit: 60,
|
||||
remaining: 50,
|
||||
resetAt: new Date(timestamp + 60000).toISOString(),
|
||||
},
|
||||
sync: {
|
||||
limit: 150,
|
||||
remaining: 45,
|
||||
resetAt: new Date(timestamp + 60000).toISOString(),
|
||||
},
|
||||
async: {
|
||||
limit: 1000,
|
||||
remaining: 50,
|
||||
resetAt: new Date(timestamp + 60000).toISOString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -149,12 +147,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
const body = JSON.stringify(payload)
|
||||
const deliveryId = `delivery_test_${uuidv4()}`
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'sim-event': 'workflow.execution.completed',
|
||||
'sim-timestamp': timestamp.toString(),
|
||||
'sim-delivery-id': `delivery_test_${uuidv4()}`,
|
||||
'Idempotency-Key': `delivery_test_${uuidv4()}`,
|
||||
'sim-delivery-id': deliveryId,
|
||||
'Idempotency-Key': deliveryId,
|
||||
}
|
||||
|
||||
if (webhook.secret) {
|
||||
|
||||
@@ -272,12 +272,14 @@ export function ChatDeploy({
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogFooter className='flex'>
|
||||
<AlertDialogCancel className='h-9 w-full rounded-[8px]' disabled={isDeleting}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
className='h-9 w-full rounded-[8px] bg-red-500 text-white transition-all duration-200 hover:bg-red-600 dark:bg-red-500 dark:hover:bg-red-600'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className='flex items-center'>
|
||||
@@ -330,6 +332,7 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('title', e.target.value)}
|
||||
required
|
||||
disabled={chatSubmitting}
|
||||
className='h-10 rounded-[8px]'
|
||||
/>
|
||||
{errors.title && <p className='text-destructive text-sm'>{errors.title}</p>}
|
||||
</div>
|
||||
@@ -344,11 +347,12 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('description', e.target.value)}
|
||||
rows={3}
|
||||
disabled={chatSubmitting}
|
||||
className='min-h-[80px] resize-none rounded-[8px]'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label className='font-medium text-sm'>Chat Output</Label>
|
||||
<Card className='rounded-md border-input shadow-none'>
|
||||
<Card className='rounded-[8px] border-input shadow-none'>
|
||||
<CardContent className='p-1'>
|
||||
<OutputSelect
|
||||
workflowId={workflowId}
|
||||
@@ -389,6 +393,7 @@ export function ChatDeploy({
|
||||
onChange={(e) => updateField('welcomeMessage', e.target.value)}
|
||||
rows={3}
|
||||
disabled={chatSubmitting}
|
||||
className='min-h-[80px] resize-none rounded-[8px]'
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
This message will be displayed when users first open the chat
|
||||
@@ -445,12 +450,14 @@ export function ChatDeploy({
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogFooter className='flex'>
|
||||
<AlertDialogCancel className='h-9 w-full rounded-[8px]' disabled={isDeleting}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className='bg-destructive hover:bg-destructive/90'
|
||||
className='h-9 w-full rounded-[8px] bg-red-500 text-white transition-all duration-200 hover:bg-red-600 dark:bg-red-500 dark:hover:bg-red-600'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className='flex items-center'>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw, Trash2 } from 'lucide-react'
|
||||
import { Button, Card, CardContent, Input, Label } from '@/components/ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn, generatePassword } from '@/lib/utils'
|
||||
import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
|
||||
|
||||
interface AuthSelectorProps {
|
||||
@@ -32,16 +32,9 @@ export function AuthSelector({
|
||||
const [emailError, setEmailError] = useState('')
|
||||
const [copySuccess, setCopySuccess] = useState(false)
|
||||
|
||||
const generatePassword = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+='
|
||||
let result = ''
|
||||
const length = 24
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
|
||||
onPasswordChange(result)
|
||||
const handleGeneratePassword = () => {
|
||||
const password = generatePassword(24)
|
||||
onPasswordChange(password)
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
@@ -80,7 +73,7 @@ export function AuthSelector({
|
||||
<Card
|
||||
key={type}
|
||||
className={cn(
|
||||
'cursor-pointer overflow-hidden shadow-none transition-colors hover:bg-accent/30',
|
||||
'cursor-pointer overflow-hidden rounded-[8px] shadow-none transition-all duration-200 hover:bg-accent/30',
|
||||
authType === type
|
||||
? 'border border-muted-foreground hover:bg-accent/50'
|
||||
: 'border border-input'
|
||||
@@ -113,7 +106,7 @@ export function AuthSelector({
|
||||
|
||||
{/* Auth Settings */}
|
||||
{authType === 'password' && (
|
||||
<Card className='shadow-none'>
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>Password Settings</h3>
|
||||
|
||||
@@ -137,42 +130,70 @@ export function AuthSelector({
|
||||
value={password}
|
||||
onChange={(e) => onPasswordChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
className='pr-28'
|
||||
className='h-10 rounded-[8px] pr-32'
|
||||
required={!isExistingChat}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<div className='absolute top-0 right-0 flex h-full'>
|
||||
<div className='absolute top-0.5 right-0.5 flex h-9 items-center gap-1 pr-1'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={generatePassword}
|
||||
size='sm'
|
||||
onClick={handleGeneratePassword}
|
||||
disabled={disabled}
|
||||
className='px-2'
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:scale-105 hover:bg-muted/50 hover:text-foreground',
|
||||
'active:scale-95',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className='h-4 w-4' />
|
||||
<RefreshCw className='h-3.5 w-3.5 transition-transform duration-200 group-hover:rotate-90' />
|
||||
<span className='sr-only'>Generate password</span>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
size='sm'
|
||||
onClick={() => copyToClipboard(password)}
|
||||
disabled={!password || disabled}
|
||||
className='px-2'
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:scale-105 hover:bg-muted/50 hover:text-foreground',
|
||||
'active:scale-95',
|
||||
'disabled:cursor-not-allowed disabled:opacity-30',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
>
|
||||
{copySuccess ? <Check className='h-4 w-4' /> : <Copy className='h-4 w-4' />}
|
||||
{copySuccess ? (
|
||||
<Check className='h-3.5 w-3.5 text-foreground' />
|
||||
) : (
|
||||
<Copy className='h-3.5 w-3.5 transition-transform duration-200 group-hover:scale-110' />
|
||||
)}
|
||||
<span className='sr-only'>Copy password</span>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
size='sm'
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
disabled={disabled}
|
||||
className='px-2'
|
||||
className={cn(
|
||||
'group h-7 w-7 rounded-md p-0',
|
||||
'text-muted-foreground/60 transition-all duration-200',
|
||||
'hover:scale-105 hover:bg-muted/50 hover:text-foreground',
|
||||
'active:scale-95',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
>
|
||||
{showPassword ? <EyeOff className='h-4 w-4' /> : <Eye className='h-4 w-4' />}
|
||||
{showPassword ? (
|
||||
<EyeOff className='h-3.5 w-3.5 transition-transform duration-200 group-hover:scale-110' />
|
||||
) : (
|
||||
<Eye className='h-3.5 w-3.5 transition-transform duration-200 group-hover:scale-110' />
|
||||
)}
|
||||
<span className='sr-only'>
|
||||
{showPassword ? 'Hide password' : 'Show password'}
|
||||
</span>
|
||||
@@ -190,7 +211,7 @@ export function AuthSelector({
|
||||
)}
|
||||
|
||||
{authType === 'email' && (
|
||||
<Card className='shadow-none'>
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>Email Access Settings</h3>
|
||||
|
||||
@@ -200,7 +221,7 @@ export function AuthSelector({
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={disabled}
|
||||
className='flex-1'
|
||||
className='h-10 flex-1 rounded-[8px]'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
@@ -212,7 +233,7 @@ export function AuthSelector({
|
||||
type='button'
|
||||
onClick={handleAddEmail}
|
||||
disabled={!newEmail.trim() || disabled}
|
||||
className='shrink-0'
|
||||
className='h-10 shrink-0 rounded-[8px]'
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
Add
|
||||
@@ -253,7 +274,7 @@ export function AuthSelector({
|
||||
)}
|
||||
|
||||
{authType === 'public' && (
|
||||
<Card className='shadow-none'>
|
||||
<Card className='rounded-[8px] shadow-none'>
|
||||
<CardContent className='p-4'>
|
||||
<h3 className='mb-2 font-medium text-sm'>Public Access Settings</h3>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
|
||||
@@ -156,7 +156,7 @@ export function ExampleCommand({
|
||||
onClick={() => setMode('sync')}
|
||||
className={`h-6 min-w-[50px] px-2 py-1 text-xs transition-none ${
|
||||
mode === 'sync'
|
||||
? 'border-primary bg-primary text-muted-foreground hover:border-primary hover:bg-primary hover:text-muted-foreground'
|
||||
? 'border-primary bg-primary text-primary-foreground hover:border-primary hover:bg-primary hover:text-primary-foreground'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
@@ -168,7 +168,7 @@ export function ExampleCommand({
|
||||
onClick={() => setMode('async')}
|
||||
className={`h-6 min-w-[50px] px-2 py-1 text-xs transition-none ${
|
||||
mode === 'async'
|
||||
? 'border-primary bg-primary text-muted-foreground hover:border-primary hover:bg-primary hover:text-muted-foreground'
|
||||
? 'border-primary bg-primary text-primary-foreground hover:border-primary hover:bg-primary hover:text-primary-foreground'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ExportControls({ disabled = false }: ExportControlsProps) {
|
||||
<TooltipTrigger asChild>
|
||||
{isDisabled ? (
|
||||
<div className='inline-flex h-12 w-12 cursor-not-allowed items-center justify-center rounded-[11px] border bg-card text-card-foreground opacity-50 shadow-xs transition-colors'>
|
||||
<Upload className='h-5 w-5' />
|
||||
<Upload className='h-4 w-4' />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -360,7 +360,12 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 p-0'
|
||||
className={cn(
|
||||
'h-8 w-8 rounded-md p-0 text-muted-foreground/70 transition-all duration-200',
|
||||
'hover:scale-105 hover:bg-muted/50 hover:text-foreground',
|
||||
'active:scale-95',
|
||||
'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1'
|
||||
)}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
@@ -374,7 +379,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className='flex flex-1 flex-col overflow-hidden'
|
||||
>
|
||||
<div className='flex-1 overflow-y-auto px-6 py-6'>
|
||||
<div className='flex-1 overflow-y-auto px-6 py-4'>
|
||||
{isLoadingTemplate ? (
|
||||
<div className='space-y-6'>
|
||||
{/* Icon and Color row */}
|
||||
@@ -414,7 +419,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-5'>
|
||||
<div className='flex gap-3'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -426,11 +431,15 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
</FormLabel>
|
||||
<Popover open={iconPopoverOpen} onOpenChange={setIconPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant='outline' role='combobox' className='h-10 w-20 p-0'>
|
||||
<Button
|
||||
variant='outline'
|
||||
role='combobox'
|
||||
className='h-10 w-20 rounded-[8px] border-border/50 p-0 transition-all duration-200 hover:border-border hover:bg-muted/50'
|
||||
>
|
||||
<SelectedIconComponent className='h-4 w-4' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='z-50 w-84 p-0' align='start'>
|
||||
<PopoverContent className='z-50 w-84 rounded-[8px] p-0' align='start'>
|
||||
<div className='p-3'>
|
||||
<div className='grid max-h-80 grid-cols-8 gap-2 overflow-y-auto'>
|
||||
{icons.map((icon) => {
|
||||
@@ -444,9 +453,10 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
setIconPopoverOpen(false)
|
||||
}}
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-md border transition-colors hover:bg-muted',
|
||||
'flex h-8 w-8 items-center justify-center rounded-md border border-border/40 transition-all duration-200',
|
||||
'hover:scale-105 hover:border-border hover:bg-muted/50 active:scale-95',
|
||||
field.value === icon.value &&
|
||||
'bg-primary text-muted-foreground'
|
||||
'border-primary/30 bg-primary/10 text-primary'
|
||||
)}
|
||||
>
|
||||
<IconComponent className='h-4 w-4' />
|
||||
@@ -475,7 +485,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
className='h-10 w-20'
|
||||
className='h-10 w-20 rounded-[8px]'
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -491,7 +501,11 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
<FormItem>
|
||||
<FormLabel className='!text-foreground font-medium text-sm'>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter template name' {...field} />
|
||||
<Input
|
||||
placeholder='Enter template name'
|
||||
className='h-10 rounded-[8px]'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -508,7 +522,11 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
Author
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter author name' {...field} />
|
||||
<Input
|
||||
placeholder='Enter author name'
|
||||
className='h-10 rounded-[8px]'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -525,7 +543,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className='h-10 rounded-[8px]'>
|
||||
<SelectValue placeholder='Select a category' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
@@ -554,7 +572,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder='Describe what this template does...'
|
||||
className='resize-none'
|
||||
className='min-h-[80px] resize-none rounded-[8px]'
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
@@ -568,7 +586,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer */}
|
||||
<div className='mt-auto border-t px-6 pt-4 pb-6'>
|
||||
<div className='mt-auto border-t px-6 py-4'>
|
||||
<div className='flex items-center'>
|
||||
{existingTemplate && (
|
||||
<Button
|
||||
@@ -576,7 +594,7 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
variant='destructive'
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
disabled={isSubmitting || isLoadingTemplate}
|
||||
className='h-10 rounded-md px-4 py-2'
|
||||
className='h-9 rounded-[8px] px-4'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -585,12 +603,11 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
type='submit'
|
||||
disabled={isSubmitting || !isFormValid || isLoadingTemplate}
|
||||
className={cn(
|
||||
'ml-auto font-medium',
|
||||
'ml-auto h-9 rounded-[8px] px-4 font-[480]',
|
||||
'bg-[var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)]',
|
||||
'shadow-[0_0_0_0_var(--brand-primary-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
|
||||
'text-white transition-all duration-200',
|
||||
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none',
|
||||
'h-10 rounded-md px-4 py-2'
|
||||
'disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none'
|
||||
)}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
@@ -618,10 +635,12 @@ export function TemplateModal({ open, onOpenChange, workflowId }: TemplateModalP
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogFooter className='flex'>
|
||||
<AlertDialogCancel className='h-9 w-full rounded-[8px]' disabled={isDeleting}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
className='h-9 w-full rounded-[8px] bg-red-500 text-white transition-all duration-200 hover:bg-red-600 dark:bg-red-500 dark:hover:bg-red-600'
|
||||
disabled={isDeleting}
|
||||
onClick={async () => {
|
||||
if (!existingTemplate) return
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -273,6 +273,22 @@ export function generateApiKey(): string {
|
||||
return `sim_${nanoid(32)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a secure random password
|
||||
* @param length - The length of the password (default: 24)
|
||||
* @returns A new secure password string
|
||||
*/
|
||||
export function generatePassword(length = 24): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+='
|
||||
let result = ''
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates through available API keys for a provider
|
||||
* @param provider - The provider to get a key for (e.g., 'openai')
|
||||
|
||||
Reference in New Issue
Block a user