234 lines
7.5 KiB
TypeScript
234 lines
7.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { AppIcon } from '@/components/ui/icons'
|
|
import { shouldShowError } from '@/lib/error-utils'
|
|
import TaskStatusInline from '@/components/task/TaskStatusInline'
|
|
import { resolveTaskPresentationState } from '@/lib/task/presentation'
|
|
import {
|
|
useAiModifyProjectPropDescription,
|
|
useAiModifyPropDescription,
|
|
useAssetActions,
|
|
} from '@/lib/query/hooks'
|
|
import { AiModifyDescriptionField } from './AiModifyDescriptionField'
|
|
|
|
export interface PropEditModalProps {
|
|
mode: 'asset-hub' | 'project'
|
|
propId: string
|
|
propName: string
|
|
summary: string
|
|
description: string
|
|
variantId?: string
|
|
projectId?: string
|
|
onClose: () => void
|
|
onRefresh?: () => void
|
|
}
|
|
|
|
export function PropEditModal({
|
|
mode,
|
|
propId,
|
|
propName,
|
|
summary,
|
|
description,
|
|
variantId,
|
|
projectId,
|
|
onClose,
|
|
onRefresh,
|
|
}: PropEditModalProps) {
|
|
const t = useTranslations('assets')
|
|
const actions = useAssetActions({
|
|
scope: mode === 'asset-hub' ? 'global' : 'project',
|
|
projectId,
|
|
kind: 'prop',
|
|
})
|
|
const [editingName, setEditingName] = useState(propName)
|
|
const [editingSummary, setEditingSummary] = useState(summary)
|
|
const [editingDescription, setEditingDescription] = useState(description)
|
|
const [aiModifyInstruction, setAiModifyInstruction] = useState('')
|
|
const [isAiModifying, setIsAiModifying] = useState(false)
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const aiModifyingState = isAiModifying
|
|
? resolveTaskPresentationState({
|
|
phase: 'processing',
|
|
intent: 'modify',
|
|
resource: 'image',
|
|
hasOutput: true,
|
|
})
|
|
: null
|
|
const savingState = isSaving
|
|
? resolveTaskPresentationState({
|
|
phase: 'processing',
|
|
intent: 'process',
|
|
resource: 'text',
|
|
hasOutput: false,
|
|
})
|
|
: null
|
|
const aiModifyAssetHub = useAiModifyPropDescription()
|
|
const aiModifyProject = useAiModifyProjectPropDescription(projectId ?? '')
|
|
|
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
|
if (error instanceof Error && error.message) return error.message
|
|
return fallback
|
|
}
|
|
|
|
const persist = async () => {
|
|
await actions.update(propId, {
|
|
name: editingName.trim(),
|
|
summary: editingSummary.trim(),
|
|
})
|
|
if (variantId) {
|
|
await actions.updateVariant(propId, variantId, {
|
|
description: editingDescription.trim(),
|
|
})
|
|
}
|
|
onRefresh?.()
|
|
}
|
|
|
|
const handleAiModify = async () => {
|
|
if (!aiModifyInstruction.trim()) return false
|
|
|
|
try {
|
|
setIsAiModifying(true)
|
|
const data = mode === 'asset-hub'
|
|
? await aiModifyAssetHub.mutateAsync({
|
|
propId,
|
|
variantId,
|
|
currentDescription: editingDescription,
|
|
modifyInstruction: aiModifyInstruction,
|
|
})
|
|
: await aiModifyProject.mutateAsync({
|
|
propId,
|
|
variantId,
|
|
currentDescription: editingDescription,
|
|
modifyInstruction: aiModifyInstruction,
|
|
})
|
|
|
|
if (data?.modifiedDescription) {
|
|
setEditingDescription(data.modifiedDescription)
|
|
setAiModifyInstruction('')
|
|
return true
|
|
}
|
|
return false
|
|
} catch (error: unknown) {
|
|
if (shouldShowError(error)) {
|
|
alert(`${t('modal.modifyFailed')}: ${getErrorMessage(error, t('errors.failed'))}`)
|
|
}
|
|
return false
|
|
} finally {
|
|
setIsAiModifying(false)
|
|
}
|
|
}
|
|
|
|
const handleSaveOnly = async () => {
|
|
if (!editingName.trim() || !editingSummary.trim() || !editingDescription.trim()) return
|
|
try {
|
|
setIsSaving(true)
|
|
await persist()
|
|
onClose()
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
const handleSaveAndGenerate = async () => {
|
|
if (!editingName.trim() || !editingSummary.trim() || !editingDescription.trim()) return
|
|
try {
|
|
setIsSaving(true)
|
|
await persist()
|
|
await actions.generate({ id: propId })
|
|
onClose()
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 glass-overlay flex items-center justify-center z-50 p-4">
|
|
<div className="glass-surface-modal max-w-2xl w-full max-h-[80vh] flex flex-col">
|
|
<div className="p-6 space-y-4 overflow-y-auto flex-1">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-[var(--glass-text-primary)]">
|
|
{t('modal.editProp')} - {propName}
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="glass-btn-base glass-btn-soft w-9 h-9 rounded-full text-[var(--glass-text-tertiary)]"
|
|
>
|
|
<AppIcon name="close" className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="glass-field-label block">
|
|
{t('prop.name')}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={editingName}
|
|
onChange={(event) => setEditingName(event.target.value)}
|
|
className="glass-input-base w-full px-3 py-2"
|
|
placeholder={t('modal.namePlaceholder')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="glass-field-label block">
|
|
{t('prop.summary')}
|
|
</label>
|
|
<textarea
|
|
value={editingSummary}
|
|
onChange={(event) => setEditingSummary(event.target.value)}
|
|
className="glass-textarea-base h-28 w-full px-3 py-2 resize-none"
|
|
placeholder={t('prop.summaryPlaceholder')}
|
|
/>
|
|
</div>
|
|
|
|
<AiModifyDescriptionField
|
|
label={t('prop.description')}
|
|
description={editingDescription}
|
|
onDescriptionChange={setEditingDescription}
|
|
descriptionPlaceholder={t('prop.descriptionPlaceholder')}
|
|
aiInstruction={aiModifyInstruction}
|
|
onAiInstructionChange={setAiModifyInstruction}
|
|
aiInstructionPlaceholder={t('modal.modifyPlaceholderProp')}
|
|
onAiModify={handleAiModify}
|
|
isAiModifying={isAiModifying}
|
|
aiModifyingState={aiModifyingState}
|
|
actionLabel={t('modal.modifyDescription')}
|
|
cancelLabel={t('common.cancel')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3 justify-end p-4 border-t border-[var(--glass-stroke-base)] bg-[var(--glass-bg-surface-strong)] rounded-b-lg flex-shrink-0">
|
|
<button
|
|
onClick={onClose}
|
|
className="glass-btn-base glass-btn-secondary px-4 py-2 rounded-lg"
|
|
disabled={isSaving}
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
onClick={() => void handleSaveOnly()}
|
|
disabled={isSaving || !editingName.trim() || !editingSummary.trim() || !editingDescription.trim()}
|
|
className="glass-btn-base glass-btn-tone-info px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isSaving ? (
|
|
<TaskStatusInline state={savingState} className="text-white [&>span]:text-white [&_svg]:text-white" />
|
|
) : (
|
|
t('modal.saveOnly')
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => void handleSaveAndGenerate()}
|
|
disabled={isSaving || !editingName.trim() || !editingSummary.trim() || !editingDescription.trim()}
|
|
className="glass-btn-base glass-btn-primary px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{t('modal.saveAndGenerate')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|