diff --git a/app/[lang]/admin/admin-shared.ts b/app/[lang]/admin/admin-shared.ts
index fc93310e..fc8b5284 100644
--- a/app/[lang]/admin/admin-shared.ts
+++ b/app/[lang]/admin/admin-shared.ts
@@ -1,3 +1,5 @@
+import type { MaskedSecret as SecretValue } from "@/lib/admin/auth"
+import type { AdminProviderInput as AdminProvider } from "@/lib/admin/providers"
import { getApiEndpoint } from "@/lib/base-path"
import type { ProviderName } from "@/lib/types/model-config"
@@ -5,7 +7,10 @@ export const SESSION_PASSWORD_KEY = "next-ai-draw-io-admin-password"
// ── Shared types ─────────────────────────────────────────────────────
-export type SecretValue = { isSet: true; hint: string }
+// The same shapes the admin API validates and returns. Secret fields of a
+// provider hold either a masked marker (unchanged) or a plaintext string
+// (new value).
+export type { AdminProvider, SecretValue }
export function isSecretValue(v: unknown): v is SecretValue {
return typeof v === "object" && v !== null && "isSet" in v
@@ -24,22 +29,6 @@ export function savedTextOf(state: SettingState | undefined): string {
return state && !isSecretValue(state.value) ? (state.value ?? "") : ""
}
-// Admin provider in client state. Secret fields hold either a masked
-// marker (unchanged) or a plaintext string (new value).
-export interface AdminProvider {
- id: string
- provider: ProviderName
- name?: string
- apiKey?: string | SecretValue
- baseUrl?: string
- awsAccessKeyId?: string | SecretValue
- awsSecretAccessKey?: string | SecretValue
- awsRegion?: string
- vertexApiKey?: string | SecretValue
- models: string[]
- isDefault?: boolean
-}
-
// Provider defined in AI_MODELS_CONFIG / ai-models.json — shown read-only
export interface EnvProvider {
name: string
diff --git a/app/[lang]/admin/setting-field.tsx b/app/[lang]/admin/setting-field.tsx
index 2bd02325..d11be1ce 100644
--- a/app/[lang]/admin/setting-field.tsx
+++ b/app/[lang]/admin/setting-field.tsx
@@ -3,13 +3,6 @@ import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useDictionary } from "@/hooks/use-dictionary"
import type { SettingDef } from "@/lib/admin/settings-registry"
@@ -206,26 +199,6 @@ export function SettingField({
)
break
}
- case "enum":
- control = (
-
- )
- break
case "secret":
control = (
diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts
index d4d09755..183c0777 100644
--- a/app/api/admin/settings/route.ts
+++ b/app/api/admin/settings/route.ts
@@ -53,10 +53,6 @@ function validateValue(def: SettingDef, value: string): string | null {
return value === "true" || value === "false"
? null
: 'Must be "true" or "false"'
- case "enum":
- return def.options?.includes(value)
- ? null
- : `Must be one of: ${def.options?.join(", ")}`
default:
return null
}
diff --git a/app/globals.css b/app/globals.css
index c4df4ee3..6cf34e3f 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -98,9 +98,7 @@
--shadow-pop:
0 1px 2px rgb(20 24 29 / 0.06), 0 18px 48px -12px rgb(20 24 29 / 0.22);
- --duration-normal: 200ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
- --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/*
@@ -241,13 +239,6 @@
.shadow-pop {
box-shadow: var(--shadow-pop);
}
- /* Older names kept for components that still use them */
- .shadow-soft {
- box-shadow: var(--shadow-float);
- }
- .shadow-dialog {
- box-shadow: var(--shadow-pop);
- }
}
@keyframes fadeIn {
@@ -284,17 +275,13 @@
}
}
-@keyframes check-pop {
- 0% {
- transform: scale(0.8);
- opacity: 0;
+/* A highlight sweeps across "Thinking..." text (Shimmer) */
+@keyframes shimmer {
+ from {
+ background-position: 100% center;
}
- 50% {
- transform: scale(1.1);
- }
- 100% {
- transform: scale(1);
- opacity: 1;
+ to {
+ background-position: 0% center;
}
}
@@ -307,15 +294,15 @@
.animate-panel-in {
animation: panelIn 0.32s var(--ease-out) both;
}
-.animate-check-pop {
- animation: check-pop 0.25s var(--ease-spring) forwards;
+.animate-shimmer {
+ animation: shimmer var(--shimmer-duration, 2s) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in,
.animate-message-in,
.animate-panel-in,
- .animate-check-pop {
+ .animate-shimmer {
animation: none;
}
}
diff --git a/components/ai-elements/model-selector.tsx b/components/ai-elements/model-selector.tsx
deleted file mode 100644
index 7164f44b..00000000
--- a/components/ai-elements/model-selector.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { Cloud } from "lucide-react"
-import type { ComponentProps, ElementRef, ReactNode } from "react"
-import { useEffect, useRef, useState } from "react"
-import {
- Command,
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- CommandShortcut,
-} from "@/components/ui/command"
-import {
- Dialog,
- DialogContent,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { cn } from "@/lib/utils"
-
-export type ModelSelectorProps = ComponentProps
-
-export const ModelSelector = (props: ModelSelectorProps) => (
-
-)
-
-export type ModelSelectorTriggerProps = ComponentProps
-
-export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
-
-)
-
-export type ModelSelectorContentProps = ComponentProps & {
- title?: ReactNode
-}
-
-export const ModelSelectorContent = ({
- className,
- children,
- title = "Model Selector",
- ...props
-}: ModelSelectorContentProps) => (
-
- {title}
-
- {children}
-
-
-)
-
-export type ModelSelectorDialogProps = ComponentProps
-
-export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
-
-)
-
-export type ModelSelectorInputProps = ComponentProps
-
-export const ModelSelectorInput = ({
- className,
- ...props
-}: ModelSelectorInputProps) => (
-
-)
-
-export type ModelSelectorListProps = ComponentProps
-
-export const ModelSelectorList = ({
- className,
- ...props
-}: ModelSelectorListProps) => {
- const listRef = useRef>(null)
- const [showShadow, setShowShadow] = useState(false)
-
- useEffect(() => {
- const listElement = listRef.current
- if (!listElement) return
-
- const checkScroll = () => {
- const { scrollTop, scrollHeight, clientHeight } = listElement
- // Show shadow if there is more content below
- // Using a small threshold to handle fractional pixel rendering
- setShowShadow(
- scrollHeight > Math.ceil(scrollTop + clientHeight) + 1,
- )
- }
-
- // Initial check
- checkScroll()
-
- // Event listeners
- listElement.addEventListener("scroll", checkScroll)
- window.addEventListener("resize", checkScroll)
-
- // Observe content changes (e.g. async loading of items)
- const observer = new MutationObserver(checkScroll)
- observer.observe(listElement, { childList: true, subtree: true })
-
- return () => {
- listElement.removeEventListener("scroll", checkScroll)
- window.removeEventListener("resize", checkScroll)
- observer.disconnect()
- }
- }, [])
-
- return (
-
-
- {/* Bottom shadow indicator for scrollable content */}
-
-
- )
-}
-
-export type ModelSelectorEmptyProps = ComponentProps
-
-export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
-
-)
-
-export type ModelSelectorGroupProps = ComponentProps
-
-export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
-
-)
-
-export type ModelSelectorItemProps = ComponentProps
-
-export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
-
-)
-
-export type ModelSelectorShortcutProps = ComponentProps
-
-export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
-
-)
-
-export type ModelSelectorSeparatorProps = ComponentProps<
- typeof CommandSeparator
->
-
-export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
-
-)
-
-export type ModelSelectorLogoProps = Omit<
- ComponentProps<"img">,
- "src" | "alt"
-> & {
- provider: string
-}
-
-export const ModelSelectorLogo = ({
- provider,
- className,
- ...props
-}: ModelSelectorLogoProps) => {
- // Use Lucide icon for bedrock since models.dev doesn't have a good AWS icon
- if (provider === "amazon-bedrock") {
- return
- }
-
- return (
- // biome-ignore lint/performance/noImgElement: External URL from models.dev
-
- )
-}
-
-export type ModelSelectorLogoGroupProps = ComponentProps<"div">
-
-export const ModelSelectorLogoGroup = ({
- className,
- ...props
-}: ModelSelectorLogoGroupProps) => (
- img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
- className,
- )}
- {...props}
- />
-)
-
-export type ModelSelectorNameProps = ComponentProps<"span">
-
-export const ModelSelectorName = ({
- className,
- ...props
-}: ModelSelectorNameProps) => (
-
-)
-
-export type ModelSelectorSectionHeaderProps = {
- icon: ReactNode
- label: string
- className?: string
-}
-
-export const ModelSelectorSectionHeader = ({
- icon,
- label,
- className,
-}: ModelSelectorSectionHeaderProps) => (
-
-
- {icon}
-
- {label}
-
-)
diff --git a/components/ai-elements/reasoning.tsx b/components/ai-elements/reasoning.tsx
index 271db5cf..b3dbc985 100644
--- a/components/ai-elements/reasoning.tsx
+++ b/components/ai-elements/reasoning.tsx
@@ -1,186 +1,83 @@
"use client"
-import { useControllableState } from "@radix-ui/react-use-controllable-state"
import { BrainIcon, ChevronDownIcon } from "lucide-react"
-import type { ComponentProps, ReactNode } from "react"
-import { createContext, memo, useContext, useEffect, useState } from "react"
+import type { ReactNode } from "react"
+import { useEffect, useState } from "react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { cn } from "@/lib/utils"
-import { Shimmer } from "./shimmer"
-type ReasoningContextValue = {
- isStreaming: boolean
- isOpen: boolean
- setIsOpen: (open: boolean) => void
- duration: number | undefined
-}
-
-const ReasoningContext = createContext
(null)
-
-export const useReasoning = () => {
- const context = useContext(ReasoningContext)
- if (!context) {
- throw new Error("Reasoning components must be used within Reasoning")
- }
- return context
-}
-
-export type ReasoningProps = ComponentProps & {
+export type ReasoningProps = {
+ className?: string
isStreaming?: boolean
- open?: boolean
+ /** Open while streaming, then folds once about a second after it ends */
defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- duration?: number
+ getThinkingMessage: (isStreaming: boolean, duration?: number) => ReactNode
+ children: string
}
const AUTO_CLOSE_DELAY = 1000
const MS_IN_S = 1000
-export const Reasoning = memo(
- ({
- className,
- isStreaming = false,
- open,
- defaultOpen = true,
- onOpenChange,
- duration: durationProp,
- children,
- ...props
- }: ReasoningProps) => {
- const [isOpen, setIsOpen] = useControllableState({
- prop: open,
- defaultProp: defaultOpen,
- onChange: onOpenChange,
- })
- const [duration, setDuration] = useControllableState({
- prop: durationProp,
- defaultProp: undefined,
- })
+export function Reasoning({
+ className,
+ isStreaming = false,
+ defaultOpen = true,
+ getThinkingMessage,
+ children,
+}: ReasoningProps) {
+ const [isOpen, setIsOpen] = useState(defaultOpen)
+ const [duration, setDuration] = useState()
+ const [hasAutoClosed, setHasAutoClosed] = useState(false)
+ const [startTime, setStartTime] = useState(null)
- const [hasAutoClosed, setHasAutoClosed] = useState(false)
- const [startTime, setStartTime] = useState(null)
-
- // Track duration when streaming starts and ends
- useEffect(() => {
- if (isStreaming) {
- if (startTime === null) {
- setStartTime(Date.now())
- }
- } else if (startTime !== null) {
- setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
- setStartTime(null)
+ // Track duration when streaming starts and ends
+ useEffect(() => {
+ if (isStreaming) {
+ if (startTime === null) {
+ setStartTime(Date.now())
}
- }, [isStreaming, startTime, setDuration])
-
- // Auto-open when streaming starts, auto-close when streaming ends (once only)
- useEffect(() => {
- if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
- // Add a small delay before closing to allow user to see the content
- const timer = setTimeout(() => {
- setIsOpen(false)
- setHasAutoClosed(true)
- }, AUTO_CLOSE_DELAY)
-
- return () => clearTimeout(timer)
- }
- }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
-
- const handleOpenChange = (newOpen: boolean) => {
- setIsOpen(newOpen)
+ } else if (startTime !== null) {
+ setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
+ setStartTime(null)
}
+ }, [isStreaming, startTime])
- return (
-
-
- {children}
-
-
- )
- },
-)
+ // Auto-close once when streaming ends, after a short delay so the user
+ // sees the content
+ useEffect(() => {
+ if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
+ const timer = setTimeout(() => {
+ setIsOpen(false)
+ setHasAutoClosed(true)
+ }, AUTO_CLOSE_DELAY)
-export type ReasoningTriggerProps = ComponentProps<
- typeof CollapsibleTrigger
-> & {
- getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
-}
+ return () => clearTimeout(timer)
+ }
+ }, [isStreaming, isOpen, defaultOpen, hasAutoClosed])
-const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
- if (isStreaming || duration === 0) {
- return Thinking...
- }
- if (duration === undefined) {
- return Thought for a few seconds
- }
- return Thought for {duration} seconds
-}
-
-export const ReasoningTrigger = memo(
- ({
- className,
- children,
- getThinkingMessage = defaultGetThinkingMessage,
- ...props
- }: ReasoningTriggerProps) => {
- const { isStreaming, isOpen, duration } = useReasoning()
-
- return (
-
- {children ?? (
- <>
-
- {getThinkingMessage(isStreaming, duration)}
-
- >
- )}
-
- )
- },
-)
-
-export type ReasoningContentProps = ComponentProps<
- typeof CollapsibleContent
-> & {
- children: string
-}
-
-export const ReasoningContent = memo(
- ({ className, children, ...props }: ReasoningContentProps) => (
-
- {children}
-
- ),
-)
-
-Reasoning.displayName = "Reasoning"
-ReasoningTrigger.displayName = "ReasoningTrigger"
-ReasoningContent.displayName = "ReasoningContent"
+
+
+ {getThinkingMessage(isStreaming, duration)}
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/components/ai-elements/shimmer.tsx b/components/ai-elements/shimmer.tsx
index 274bf85f..1ebed4f0 100644
--- a/components/ai-elements/shimmer.tsx
+++ b/components/ai-elements/shimmer.tsx
@@ -1,13 +1,6 @@
"use client"
-import { motion } from "motion/react"
-import {
- type CSSProperties,
- type ElementType,
- type JSX,
- memo,
- useMemo,
-} from "react"
+import { type CSSProperties, type ElementType, memo, useMemo } from "react"
import { cn } from "@/lib/utils"
export type TextShimmerProps = {
@@ -25,39 +18,29 @@ const ShimmerComponent = ({
duration = 2,
spread = 2,
}: TextShimmerProps) => {
- const MotionComponent = motion.create(
- Component as keyof JSX.IntrinsicElements,
- )
-
const dynamicSpread = useMemo(
() => (children?.length ?? 0) * spread,
[children, spread],
)
return (
-
{children}
-
+
)
}
diff --git a/components/button-with-tooltip.tsx b/components/button-with-tooltip.tsx
deleted file mode 100644
index 089b0596..00000000
--- a/components/button-with-tooltip.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { VariantProps } from "class-variance-authority"
-import type React from "react"
-import { Button, type buttonVariants } from "@/components/ui/button"
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-
-interface ButtonWithTooltipProps
- extends React.ComponentProps<"button">,
- VariantProps {
- tooltipContent: string
- children: React.ReactNode
- asChild?: boolean
-}
-
-export function ButtonWithTooltip({
- tooltipContent,
- children,
- ...buttonProps
-}: ButtonWithTooltipProps) {
- return (
-
-
-
-
-
-
- {tooltipContent}
-
-
-
- )
-}
diff --git a/components/chat/TemplateCreateDialog.tsx b/components/chat/TemplateCreateDialog.tsx
deleted file mode 100644
index c88a5be9..00000000
--- a/components/chat/TemplateCreateDialog.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-"use client"
-
-import { Bookmark, Plus } from "lucide-react"
-import { useEffect, useState } from "react"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Switch } from "@/components/ui/switch"
-import { Textarea } from "@/components/ui/textarea"
-import { useDictionary } from "@/hooks/use-dictionary"
-import {
- createTemplate,
- type TemplateCreateInput,
-} from "@/lib/template-storage"
-
-interface TemplateCreateDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- onSuccess: () => void
- initialPrompt?: string
-}
-
-export function TemplateCreateDialog({
- open,
- onOpenChange,
- onSuccess,
- initialPrompt = "",
-}: TemplateCreateDialogProps) {
- const dict = useDictionary()
- const [title, setTitle] = useState("")
- const [description, setDescription] = useState("")
- const [prompt, setPrompt] = useState("")
- const [pinned, setPinned] = useState(false)
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [error, setError] = useState(null)
-
- // Reset form when dialog opens with the latest initialPrompt
- useEffect(() => {
- if (open) {
- setTitle("")
- setDescription("")
- setPrompt(initialPrompt)
- setPinned(false)
- setError(null)
- }
- }, [open, initialPrompt])
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- // React submit events bubble through the portal; keep them away from
- // the chat form this dialog may be rendered in
- e.stopPropagation()
-
- const trimmedPrompt = prompt.trim()
- if (!trimmedPrompt) {
- setError(dict.templates.promptRequired)
- return
- }
-
- setIsSubmitting(true)
- setError(null)
-
- try {
- const input: TemplateCreateInput = {
- prompt: trimmedPrompt,
- title: title.trim() || undefined,
- description: description.trim() || undefined,
- pinned,
- }
-
- const template = await createTemplate(input)
- if (template) {
- onSuccess()
- onOpenChange(false)
- } else {
- setError(dict.templates.createFailed)
- }
- } catch (err) {
- console.error("Failed to create template:", err)
- setError(dict.templates.createFailed)
- } finally {
- setIsSubmitting(false)
- }
- }
-
- return (
-
- )
-}
diff --git a/components/chat/TemplateEditDialog.tsx b/components/chat/TemplateFormDialog.tsx
similarity index 68%
rename from components/chat/TemplateEditDialog.tsx
rename to components/chat/TemplateFormDialog.tsx
index 3a380034..40a3b7ba 100644
--- a/components/chat/TemplateEditDialog.tsx
+++ b/components/chat/TemplateFormDialog.tsx
@@ -1,7 +1,7 @@
"use client"
-import { Bookmark, Edit2 } from "lucide-react"
-import { useEffect, useState } from "react"
+import { Bookmark, Edit2, Plus } from "lucide-react"
+import { useEffect, useId, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -16,22 +16,32 @@ import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import { useDictionary } from "@/hooks/use-dictionary"
-import { type Template, updateTemplate } from "@/lib/template-storage"
+import {
+ createTemplate,
+ type Template,
+ updateTemplate,
+} from "@/lib/template-storage"
-interface TemplateEditDialogProps {
+interface TemplateFormDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
- template: Template | null
onSuccess: () => void
+ /** The template to edit; without one the dialog creates a new template */
+ template?: Template | null
+ initialPrompt?: string
}
-export function TemplateEditDialog({
+// One form for creating and editing a template
+export function TemplateFormDialog({
open,
onOpenChange,
- template,
onSuccess,
-}: TemplateEditDialogProps) {
+ template,
+ initialPrompt = "",
+}: TemplateFormDialogProps) {
const dict = useDictionary()
+ const id = useId()
+ const isEdit = !!template
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [prompt, setPrompt] = useState("")
@@ -39,32 +49,22 @@ export function TemplateEditDialog({
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState(null)
- // Populate form each time the dialog opens, dropping any cancelled edits
+ // Fill the form each time the dialog opens, dropping cancelled edits
useEffect(() => {
- if (open && template) {
- setTitle(template.title || "")
- setDescription(template.description || "")
- setPrompt(template.prompt || "")
- setPinned(template.pinned || false)
- setError(null)
- }
- }, [open, template])
-
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen) {
- setError(null)
- }
- onOpenChange(newOpen)
- }
+ if (!open) return
+ setTitle(template?.title || "")
+ setDescription(template?.description || "")
+ setPrompt(template ? template.prompt || "" : initialPrompt)
+ setPinned(template?.pinned || false)
+ setError(null)
+ }, [open, template, initialPrompt])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// React submit events bubble through the portal; keep them away from
- // any form this dialog may be rendered in
+ // the chat form this dialog may be rendered in
e.stopPropagation()
- if (!template) return
-
const trimmedPrompt = prompt.trim()
if (!trimmedPrompt) {
setError(dict.templates.promptRequired)
@@ -74,40 +74,59 @@ export function TemplateEditDialog({
setIsSubmitting(true)
setError(null)
+ const failed = isEdit
+ ? dict.templates.updateFailed
+ : dict.templates.createFailed
try {
- const updates: Partial> = {
+ const fields = {
prompt: trimmedPrompt,
- title: title.trim() || template.title,
description: description.trim() || undefined,
pinned,
}
-
- const updated = await updateTemplate(template.id, updates)
- if (updated) {
+ // An emptied title keeps the old one when editing; a new template
+ // gets a default title from its prompt
+ const result = template
+ ? await updateTemplate(template.id, {
+ ...fields,
+ title: title.trim() || template.title,
+ })
+ : await createTemplate({
+ ...fields,
+ title: title.trim() || undefined,
+ })
+ if (result) {
onSuccess()
onOpenChange(false)
} else {
- setError(dict.templates.updateFailed)
+ setError(failed)
}
} catch (err) {
- console.error("Failed to update template:", err)
- setError(dict.templates.updateFailed)
+ console.error("Failed to save template:", err)
+ setError(failed)
} finally {
setIsSubmitting(false)
}
}
return (
-