Files
hapi/web/src/components/RenameSessionDialog.tsx
T
AnanovoandGitHub ad72229923 feat(sessions): add on-demand AI title suggestions (#1577)
* feat(sessions): add on-demand AI title suggestions

* fix(sessions): address title suggestion review feedback

* fix(web): ignore stale title generation results
2026-08-15 11:13:49 +08:00

172 lines
6.2 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { useTranslation } from '@/lib/use-translation'
type RenameSessionDialogProps = {
isOpen: boolean
onClose: () => void
currentName: string
onRename: (newName: string) => Promise<void>
onSuggestTitle?: () => Promise<string>
onUpdateSummary?: (text: string) => Promise<void>
isPending: boolean
}
export function RenameSessionDialog(props: RenameSessionDialogProps) {
const { t } = useTranslation()
const { isOpen, onClose, currentName, onRename, onSuggestTitle, onUpdateSummary, isPending } = props
const [name, setName] = useState(currentName)
const [error, setError] = useState<string | null>(null)
const [draftSource, setDraftSource] = useState<'manual' | 'generated'>('manual')
const [draftEdited, setDraftEdited] = useState(false)
const [isGenerating, setIsGenerating] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
const generationRef = useRef(0)
const busy = isPending || isGenerating
const handleClose = () => {
generationRef.current += 1
setIsGenerating(false)
onClose()
}
useEffect(() => {
generationRef.current += 1
if (!isOpen) {
setIsGenerating(false)
return
}
if (isOpen) {
setName(currentName)
setError(null)
setDraftSource('manual')
setDraftEdited(false)
setIsGenerating(false)
setTimeout(() => {
inputRef.current?.focus()
inputRef.current?.select()
}, 100)
}
}, [isOpen, currentName])
const handleGenerate = async () => {
if (!onSuggestTitle) return
const generation = ++generationRef.current
setError(null)
setIsGenerating(true)
try {
const suggested = (await onSuggestTitle()).trim()
if (generation !== generationRef.current) return
if (!suggested) throw new Error('Empty title suggestion')
setName(suggested)
setDraftSource('generated')
} catch {
if (generation === generationRef.current) {
setError(t('dialog.rename.generateError'))
}
} finally {
if (generation === generationRef.current) {
setIsGenerating(false)
}
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const trimmed = name.trim()
if (!trimmed || (!draftEdited && draftSource === 'manual' && trimmed === currentName)) {
handleClose()
return
}
setError(null)
try {
if (draftSource === 'generated' && onUpdateSummary) {
await onUpdateSummary(trimmed)
} else {
await onRename(trimmed)
}
handleClose()
} catch {
setError(t('dialog.rename.error'))
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose()
}
}
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent className="max-w-sm">
<DialogHeader className="pr-0">
<DialogTitle className="min-h-6 px-10 text-center leading-6">
{t('dialog.rename.title')}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="mt-4 flex flex-col gap-4">
<input
ref={inputRef}
type="text"
value={name}
onChange={(e) => {
setName(e.target.value)
setDraftSource('manual')
setDraftEdited(true)
}}
onKeyDown={handleKeyDown}
placeholder={t('dialog.rename.placeholder')}
className="w-full px-3 py-2.5 rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none focus:ring-2 focus:ring-[var(--app-button)] focus:border-transparent"
disabled={busy}
maxLength={255}
/>
{error ? (
<div className="rounded-md bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/20 dark:text-red-400">
{error}
</div>
) : null}
<div className="flex items-center justify-between gap-2">
<Button
type="button"
variant="secondary"
onClick={handleClose}
disabled={busy}
>
{t('button.cancel')}
</Button>
<div className="flex gap-2">
{onSuggestTitle ? (
<Button
type="button"
variant="outline"
onClick={() => void handleGenerate()}
disabled={busy}
>
{isGenerating ? t('dialog.rename.generating') : t('dialog.rename.generate')}
</Button>
) : null}
<Button
type="submit"
disabled={busy || !name.trim()}
>
{isPending ? t('dialog.rename.saving') : t('button.save')}
</Button>
</div>
</div>
</form>
</DialogContent>
</Dialog>
)
}