* feat(i18n): complete 100% internationalization and fix Next.js 15 compatibility
* feat(i18n): complete 100% internationalization coverage
* chore(test): finalize component tests and project cleanup
* test(logic): add unit tests for useModalManager hook
* fix(test): resolve timeout in AppSidebar tests by mocking TooltipProvider
* feat(i18n): comprehensive i18n audit, fixes for hardcoded strings, and complete zh-TW support
* fix(i18n): resolve TypeScript warnings and improve translation hook stability
- Remove unused useTranslation import from ConnectionGuard
- Add ref-based checking state to prevent dependency cycles
- Fix useTranslation hook to return empty string for undefined translations
- Add comment for backward compatibility on ExtractedReference interface
- Ensure .replace() string methods work safely with nested translation keys
* feat(i18n): complete internationalization implementation with Docker deployment
- Add LanguageLoadingOverlay component for smooth language transitions
- Update all translation files (en-US, zh-CN, zh-TW) with improved terminology
- Optimize Docker configuration for better performance
- Update version check and config handling for i18n support
- Fix route handling for language-specific content
- Add comprehensive task documentation
* fix(i18n): resolve localization errors, duplicates, and type issues
* chore(i18n): finalize 100% internationalization coverage
* chore(test): supplement i18n test cases and cleanup redundant files
* fix(test): resolve lint type errors and finalize delivery documents
* feat(i18n): finalize full internationalization and zh-TW localization
* fix(frontend): add missing devDependency and fix build tsconfig
* feat(ui): enhance sidebar hover effects with better visual feedback
* fix(frontend): resolve accessibility, i18n, and lint issues
- fix: add missing id, name, autocomplete attributes to dialog inputs
- fix: add aria labels and DialogDescription for accessibility
- fix: resolve uncontrolled component warning in SettingsForm
- fix: correct duplicate 'Traditional Chinese' label in zh-TW locale
- feat: add i18n support for podcast template names
- chore: fix lint errors in Dialogs
* fix: address all 21 PR feedback items from cubic-dev-ai bot
Configuration:
- Remove ignoreDuringBuilds flags from next.config.ts
Testing:
- Fix AppSidebar.test.tsx regex pattern and add missing assertion
Logic:
- Fix ConnectionGuard.tsx re-entry prevention logic
Internationalization (I18n) - Translations:
- Add missing keys: notebooks.archived, common.note/insight, accessibility keys
- Add specific keys: sources.allSourcesDescShort, transformations.selectModel
- Add singular/plural keys: podcasts.usedByCount_one/other, common.note/notes
- Add common.created/updated with {time} placeholder
Internationalization (I18n) - Usage:
- SourcesPage: use allSourcesDescShort instead of string splitting
- TransformationPlayground: use navigation.transformation and selectModel
- CommandPalette: use dedicated keys instead of string concatenation
- GeneratePodcastDialog: fix zh-TW date locale handling
- NotebookHeader: correctly interpolate {time} placeholder
- TransformationCard: use common.description instead of undefined key
- ChatPanel/SpeakerProfilesPanel: implement proper pluralization
- SystemInfo: correctly interpolate {version} placeholder
- LanguageLoadingOverlay: use t.common.loading instead of hardcoded string
- MessageActions: use specific error key cannotSaveNoteNoNotebook
Other:
- Fix SessionManager.tsx exhaustive-deps warning
* fix: remove duplicate locale keys and add missing zh-CN translations
- en-US: remove duplicate loading key (line 59) and addNew key (sources)
- zh-CN: remove duplicate common keys (loading, note, insight, newSource, newNotebook, newPodcast)
- zh-CN: remove duplicate accessibility.searchNotebooks key
- zh-CN: remove duplicate sources.addNew key
- zh-CN: remove duplicate navigation.transformation key
- zh-CN: add missing usedByCount_one and usedByCount_other keys in podcasts
- zh-TW: remove duplicate common keys (loading, note, insight, newSource, newNotebook, newPodcast)
- zh-TW: remove duplicate accessibility.searchNotebooks key
- zh-TW: remove duplicate sources.addNew key
* docs: remove info.md
* fix: remove duplicate notebook keys and unused ts-expect-error
- zh-CN: remove duplicate notebooks keys (archived, archive, unarchive, deleteNotebook, deleteNotebookDesc)
- zh-TW: remove duplicate notebooks keys (archived, archive, unarchive, deleteNotebook, deleteNotebookDesc)
- GeneratePodcastDialog: remove unused @ts-expect-error directive
* fix(a11y): fix unassociated labels in search page
- Replace <Label> with role='group' + aria-labelledby for search type section
- Replace <Label> with role='group' + aria-labelledby for search in section
- Follows WAI-ARIA best practices for labeling form field groups
* fix(a11y): fix unassociated labels across multiple components
- search/page.tsx: use role='group' + aria-labelledby for search type and search in sections
- RebuildEmbeddings.tsx: use role='group' + aria-labelledby for include checkboxes
- TransformationPlayground.tsx: replace Label with span for non-form output label
* chore: revert to npm stack and ensure i18n compatibility
* chore: polish zh-TW translations for better idiomatic usage
* fix: resolve linter errors (ruff import sort, mypy config duplicate)
* style: apply ruff formatting
* fix: finalize upstream compliance (Dockerfile.single, i18n hooks, docker-compose)
* style: polish strings, fix timeout cleanup, and improve test mocks
* fix: use relative imports in test setup to resolve IDE path errors
* perf(docker): optimize build speed by removing apt-get upgrade and build tools
- Remove apt-get upgrade from both builder and runtime stages (saves 10-15 min each)
- Remove gcc/g++/make/git from builder (uv downloads pre-built wheels)
- Add --no-install-recommends to minimize package footprint
- Keep npm mirror (npmmirror.com) for faster frontend deps
- Add npm registry config for reliable China network access
Also includes:
- fix(a11y): add missing labels and aria attributes to form fields
- fix(i18n): add 2s safety timeout to LanguageLoadingOverlay
- fix(i18n): add robustness checks to use-translation proxy
Build time reduced from 2+ hours to ~34 minutes (~70% improvement)
* fix(a11y): resolve 16 form field accessibility warnings in notebook and podcast pages
* fix(a11y): resolve 4 button and 1 select field accessibility warnings in models page
* fix(a11y): resolve redundant attributes and residual warnings in transformations and podcast forms
* fix(i18n): deep fix for language switch hang using proxy protection and safer access
* fix(a11y): add name attributes to ModelSelector, TransformationPlayground, and SourceDetailContent
* fix: add missing Label import to SourceDetailContent
* fix(i18n): use native react-i18next in LanguageLoadingOverlay to prevent hang during language switch
* fix(i18n): rewrite use-translation Proxy with strict depth limit and expanded blocked props to prevent language switch hang
* fix: add type assertion to fix TypeScript comparison error
* fix(i18n): disable useSuspense to prevent thread hang during language resource loading
* fix(i18n): add infinite loop detection circuit breaker to useTranslation hook
* fix(i18n): update traditional chinese label to native script in en-US
* feat: add new localization strings for notebook and note management.
* fix: resolve config priority, docker build deps, and ui glitches
* refactor: improve ui details and test coverage based on feedback
* refactor: improve ui details (version check/lang toggle) and test coverage
* fix: polish language matching and test cleanup
* fix(test): update mocks to resolve timeouts and proxy errors
* fix(frontend): restore tsconfig.json structure and enable IDE support for tests
* fix: address PR review findings and resolve CI OIDC failure
* fix: merge exception headers in custom handler
* fix: comprehensive PR review remediations and async performance fixes
* refactor: address all PR #371 review feedback
- Docker: consolidate SURREAL_URL to docker.env, add single-container override
- Security: restore apt-get upgrade in Dockerfile and Dockerfile.single
- Create centralized getDateLocale helper (lib/utils/date-locale.ts)
- Refactor 7 files to use getDateLocale helper
- Revert config/route.ts to origin/main version
- Move test files to co-located pattern (3 files)
- Remove local useTranslation mock from ConfirmDialog.test.tsx
- Simplify use-version-check to single useEffect pattern
- Fix test import paths after moving to co-located pattern
* fix: add jest-dom types for test files
* fix: address remaining review issues
- Add apt-get upgrade -y to Dockerfile.single backend-builder stage
- Refactor ChatColumn.test.tsx: use 'as unknown as ReturnType<typeof hook>' instead of 'as any'
- Use toBeInTheDocument() assertions instead of toBeDefined()
451 lines
16 KiB
TypeScript
451 lines
16 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useMemo } from 'react'
|
|
import { Controller, useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
|
|
import { EpisodeProfile, SpeakerProfile } from '@/lib/types/podcasts'
|
|
import {
|
|
useCreateEpisodeProfile,
|
|
useUpdateEpisodeProfile,
|
|
} from '@/lib/hooks/use-podcasts'
|
|
import { useTranslation } from '@/lib/hooks/use-translation'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
|
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 { Textarea } from '@/components/ui/textarea'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import { TranslationKeys } from '@/lib/locales'
|
|
|
|
const episodeProfileSchema = (t: TranslationKeys) => z.object({
|
|
name: z.string().min(1, t.podcasts.nameRequired || 'Name is required'),
|
|
description: z.string().optional(),
|
|
speaker_config: z.string().min(1, t.podcasts.profileRequired || 'Speaker profile is required'),
|
|
outline_provider: z.string().min(1, t.podcasts.outlineProviderRequired || 'Outline provider is required'),
|
|
outline_model: z.string().min(1, t.podcasts.outlineModelRequired || 'Outline model is required'),
|
|
transcript_provider: z.string().min(1, t.podcasts.transcriptProviderRequired || 'Transcript provider is required'),
|
|
transcript_model: z.string().min(1, t.podcasts.transcriptModelRequired || 'Transcript model is required'),
|
|
default_briefing: z.string().min(1, t.podcasts.defaultBriefingRequired || 'Default briefing is required'),
|
|
num_segments: z.number()
|
|
.int(t.podcasts.segmentsInteger || 'Must be an integer')
|
|
.min(3, t.podcasts.segmentsMin || 'At least 3 segments')
|
|
.max(20, t.podcasts.segmentsMax || 'Maximum 20 segments'),
|
|
})
|
|
|
|
export type EpisodeProfileFormValues = z.infer<ReturnType<typeof episodeProfileSchema>>
|
|
|
|
interface EpisodeProfileFormDialogProps {
|
|
mode: 'create' | 'edit'
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
speakerProfiles: SpeakerProfile[]
|
|
modelOptions: Record<string, string[]>
|
|
initialData?: EpisodeProfile
|
|
}
|
|
|
|
export function EpisodeProfileFormDialog({
|
|
mode,
|
|
open,
|
|
onOpenChange,
|
|
speakerProfiles,
|
|
modelOptions,
|
|
initialData,
|
|
}: EpisodeProfileFormDialogProps) {
|
|
const { t } = useTranslation()
|
|
const createProfile = useCreateEpisodeProfile()
|
|
const updateProfile = useUpdateEpisodeProfile()
|
|
|
|
const providers = useMemo(() => Object.keys(modelOptions), [modelOptions])
|
|
|
|
const getDefaults = useCallback((): EpisodeProfileFormValues => {
|
|
const firstSpeaker = speakerProfiles[0]?.name ?? ''
|
|
const firstProvider = providers[0] ?? ''
|
|
const firstModel = firstProvider ? modelOptions[firstProvider]?.[0] ?? '' : ''
|
|
|
|
if (initialData) {
|
|
return {
|
|
name: initialData.name,
|
|
description: initialData.description ?? '',
|
|
speaker_config: initialData.speaker_config,
|
|
outline_provider: initialData.outline_provider,
|
|
outline_model: initialData.outline_model,
|
|
transcript_provider: initialData.transcript_provider,
|
|
transcript_model: initialData.transcript_model,
|
|
default_briefing: initialData.default_briefing,
|
|
num_segments: initialData.num_segments,
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: '',
|
|
description: '',
|
|
speaker_config: firstSpeaker,
|
|
outline_provider: firstProvider,
|
|
outline_model: firstModel,
|
|
transcript_provider: firstProvider,
|
|
transcript_model: firstModel,
|
|
default_briefing: '',
|
|
num_segments: 5,
|
|
}
|
|
}, [initialData, modelOptions, providers, speakerProfiles])
|
|
|
|
const {
|
|
control,
|
|
register,
|
|
handleSubmit,
|
|
reset,
|
|
setValue,
|
|
watch,
|
|
formState: { errors },
|
|
} = useForm<EpisodeProfileFormValues>({
|
|
resolver: zodResolver(episodeProfileSchema(t)),
|
|
defaultValues: getDefaults(),
|
|
})
|
|
|
|
const outlineProvider = watch('outline_provider')
|
|
const outlineModel = watch('outline_model')
|
|
const transcriptProvider = watch('transcript_provider')
|
|
const transcriptModel = watch('transcript_model')
|
|
const availableOutlineModels = modelOptions[outlineProvider] ?? []
|
|
const availableTranscriptModels = modelOptions[transcriptProvider] ?? []
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
return
|
|
}
|
|
reset(getDefaults())
|
|
}, [open, reset, getDefaults])
|
|
|
|
useEffect(() => {
|
|
if (!outlineProvider) {
|
|
return
|
|
}
|
|
const models = modelOptions[outlineProvider] ?? []
|
|
if (models.length === 0) {
|
|
setValue('outline_model', '')
|
|
return
|
|
}
|
|
if (!models.includes(outlineModel)) {
|
|
setValue('outline_model', models[0])
|
|
}
|
|
}, [outlineProvider, outlineModel, modelOptions, setValue])
|
|
|
|
useEffect(() => {
|
|
if (!transcriptProvider) {
|
|
return
|
|
}
|
|
const models = modelOptions[transcriptProvider] ?? []
|
|
if (models.length === 0) {
|
|
setValue('transcript_model', '')
|
|
return
|
|
}
|
|
if (!models.includes(transcriptModel)) {
|
|
setValue('transcript_model', models[0])
|
|
}
|
|
}, [transcriptProvider, transcriptModel, modelOptions, setValue])
|
|
|
|
const onSubmit = async (values: EpisodeProfileFormValues) => {
|
|
const payload = {
|
|
...values,
|
|
description: values.description ?? '',
|
|
}
|
|
|
|
if (mode === 'create') {
|
|
await createProfile.mutateAsync(payload)
|
|
} else if (initialData) {
|
|
await updateProfile.mutateAsync({
|
|
profileId: initialData.id,
|
|
payload,
|
|
})
|
|
}
|
|
|
|
onOpenChange(false)
|
|
}
|
|
|
|
const isSubmitting = createProfile.isPending || updateProfile.isPending
|
|
const disableSubmit =
|
|
isSubmitting || speakerProfiles.length === 0 || providers.length === 0
|
|
const isEdit = mode === 'edit'
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{isEdit ? t.podcasts.editEpisodeProfile : t.podcasts.createEpisodeProfile}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{t.podcasts.episodeProfileFormDesc}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{speakerProfiles.length === 0 ? (
|
|
<Alert className="bg-amber-50 text-amber-900 border-amber-200">
|
|
<AlertTitle>{t.podcasts.noSpeakerProfilesAvailable}</AlertTitle>
|
|
<AlertDescription>
|
|
{t.podcasts.noSpeakerProfilesDesc}
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
{providers.length === 0 ? (
|
|
<Alert className="bg-amber-50 text-amber-900 border-amber-200">
|
|
<AlertTitle>{t.podcasts.noLanguageModelsAvailable}</AlertTitle>
|
|
<AlertDescription>
|
|
{t.podcasts.noLanguageModelsDesc}
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : null}
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 pt-2">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">{t.podcasts.profileName} *</Label>
|
|
<Input id="name" placeholder={t.podcasts.profileNamePlaceholder} {...register('name')} />
|
|
{errors.name ? (
|
|
<p className="text-xs text-red-600">{errors.name.message}</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="num_segments">{t.podcasts.segments} *</Label>
|
|
<Input
|
|
id="num_segments"
|
|
type="number"
|
|
min={3}
|
|
max={20}
|
|
{...register('num_segments', { valueAsNumber: true })}
|
|
autoComplete="off"
|
|
/>
|
|
{errors.num_segments ? (
|
|
<p className="text-xs text-red-600">{errors.num_segments.message}</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="md:col-span-2 space-y-2">
|
|
<Label htmlFor="description">{t.common.description}</Label>
|
|
<Textarea
|
|
id="description"
|
|
rows={3}
|
|
placeholder={t.podcasts.descriptionPlaceholder}
|
|
{...register('description')}
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{t.podcasts.speakerConfig}
|
|
</h3>
|
|
<Separator className="mt-2" />
|
|
</div>
|
|
<Controller
|
|
control={control}
|
|
name="speaker_config"
|
|
render={({ field }) => (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="speaker_config">{t.podcasts.speakerProfile} *</Label>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger id="speaker_config">
|
|
<SelectValue placeholder={t.podcasts.selectSpeakerProfile} />
|
|
</SelectTrigger>
|
|
<SelectContent title={t.podcasts.speakerProfile}>
|
|
{speakerProfiles.map((profile) => (
|
|
<SelectItem key={profile.id} value={profile.name}>
|
|
{profile.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.speaker_config ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.speaker_config.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{t.podcasts.outlineGeneration}
|
|
</h3>
|
|
<Separator className="mt-2" />
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<Controller
|
|
control={control}
|
|
name="outline_provider"
|
|
render={({ field }) => (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="outline_provider">{t.models.provider} *</Label>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger id="outline_provider">
|
|
<SelectValue placeholder={t.models.selectProviderPlaceholder} />
|
|
</SelectTrigger>
|
|
<SelectContent title={t.models.provider}>
|
|
{providers.map((provider) => (
|
|
<SelectItem key={provider} value={provider}>
|
|
<span className="capitalize">{provider}</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.outline_provider ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.outline_provider.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="outline_model"
|
|
render={({ field }) => (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="outline_model">{t.common.model} *</Label>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger id="outline_model">
|
|
<SelectValue placeholder={t.models.selectModelPlaceholder} />
|
|
</SelectTrigger>
|
|
<SelectContent title={t.common.model}>
|
|
{availableOutlineModels.map((model) => (
|
|
<SelectItem key={model} value={model}>
|
|
{model}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.outline_model ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.outline_model.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{t.podcasts.transcriptGeneration}
|
|
</h3>
|
|
<Separator className="mt-2" />
|
|
</div>
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<Controller
|
|
control={control}
|
|
name="transcript_provider"
|
|
render={({ field }) => (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="transcript_provider">{t.models.provider} *</Label>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger id="transcript_provider">
|
|
<SelectValue placeholder={t.models.selectProviderPlaceholder} />
|
|
</SelectTrigger>
|
|
<SelectContent title={t.models.provider}>
|
|
{providers.map((provider) => (
|
|
<SelectItem key={provider} value={provider}>
|
|
<span className="capitalize">{provider}</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.transcript_provider ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.transcript_provider.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
name="transcript_model"
|
|
render={({ field }) => (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="transcript_model">{t.common.model} *</Label>
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger id="transcript_model">
|
|
<SelectValue placeholder={t.models.selectModelPlaceholder} />
|
|
</SelectTrigger>
|
|
<SelectContent title={t.common.model}>
|
|
{availableTranscriptModels.map((model) => (
|
|
<SelectItem key={model} value={model}>
|
|
{model}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.transcript_model ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.transcript_model.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="default_briefing">{t.podcasts.defaultBriefingTitle} *</Label>
|
|
<Textarea
|
|
id="default_briefing"
|
|
rows={6}
|
|
placeholder={t.podcasts.defaultBriefingPlaceholder}
|
|
{...register('default_briefing')}
|
|
/>
|
|
{errors.default_briefing ? (
|
|
<p className="text-xs text-red-600">
|
|
{errors.default_briefing.message}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
{t.common.cancel}
|
|
</Button>
|
|
<Button type="submit" disabled={disableSubmit}>
|
|
{isSubmitting
|
|
? t.common.saving
|
|
: isEdit
|
|
? t.common.saveChanges
|
|
: t.podcasts.createProfile}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|