@@ -31,7 +31,6 @@ function inputAppliesToVersion(input: IModelInputs, currentVersion: string): boo return input.versions.length === 0 || input.versions.includes(currentVersion) } -/** Учитывает только inputs, актуальные для версии — иначе required с другой версии (второй image) ломает валидацию */ function buildRequiredForVersion(inputs: IModelInputs[], currentVersion: string): (string | null)[] { const requiredTypes = new Set() for (const el of inputs) { @@ -43,7 +42,6 @@ function buildRequiredForVersion(inputs: IModelInputs[], currentVersion: string) interface Input { loading: boolean - // requestImage: () => void wonderMe?: () => void desktop: boolean image?: File | null @@ -51,10 +49,7 @@ interface Input { quality?: string unpinImage?: () => void imageLoad?: (event: React.ChangeEvent | null, file?: File) => void - // openFilters: (event: React.MouseEvent) => void sendMessage: (message: string, required: (string | null)[]) => boolean - // setInput: React.Dispatch> - // setImage: React.Dispatch> styles: 'images' | 'chats' | 'audio' input_types?: IModelInputs[] blocked?: boolean @@ -96,10 +91,25 @@ export const ModelInput: FC = ({ return Array.isArray(versions) && (versions.length === 0 || versions.includes(currentVersion)) }) - // Используем внешнее значение если оно передано, иначе внутреннее const value = externalValue !== undefined ? externalValue : internalValue const setValue = externalOnChange ? (val: string) => externalOnChange(val) : setInternalValue + const draftStorageKey = React.useMemo(() => { + return `model-input:draft:${styles}:${currentVersion}` + }, [styles, currentVersion]) + + const draftHydratedRef = useRef(false) + const setValueRef = useRef(setValue) + setValueRef.current = setValue + + const clearDraft = useCallback(() => { + try { + window.localStorage.removeItem(draftStorageKey) + } catch { + } + setValue('') + }, [draftStorageKey, setValue]) + const valueRef = useRef(value) const requiredRef = useRef(required) valueRef.current = value @@ -134,7 +144,6 @@ export const ModelInput: FC = ({ const name = (file.name || '').toLowerCase().split('?')[0] const isMp4 = name.endsWith('.mp4') || file.type === 'video/mp4' - // Только mp4 (для чисто-video моделей и для любых video/* файлов) if ((types.length === 1 && types[0] === 'video' && !isMp4) || (file.type.startsWith('video/') && !isMp4)) { showMessage('Можно загрузить только видео в формате mp4') return @@ -171,6 +180,28 @@ export const ModelInput: FC = ({ } }, [resendValue, setValue]) + useEffect(() => { + draftHydratedRef.current = false + }, [draftStorageKey]) + + useEffect(() => { + try { + if (!draftHydratedRef.current) { + draftHydratedRef.current = true + if (resendValue) { + return + } + const saved = window.localStorage.getItem(draftStorageKey) + if (saved !== null && saved !== '' && value === '') { + setValueRef.current(saved) + return + } + } + window.localStorage.setItem(draftStorageKey, value) + } catch { + } + }, [draftStorageKey, resendValue, value]) + useEffect(() => { const handleTourSuggestedQuery = (e: CustomEvent<{ query: string }>) => { if (e.detail?.query) { @@ -188,14 +219,14 @@ export const ModelInput: FC = ({ const { current: msg } = valueRef const { current: req } = requiredRef if (msg && sendMessage(msg, req)) { - setValue('') + clearDraft() if (unpinImage) unpinImage() window.dispatchEvent(new CustomEvent('user-sent-message')) } } window.addEventListener('tour-send-message', handleTourSendMessage) return () => window.removeEventListener('tour-send-message', handleTourSendMessage) - }, [sendMessage, setValue, unpinImage]) + }, [clearDraft, sendMessage, unpinImage]) return ( <> @@ -235,7 +266,7 @@ export const ModelInput: FC = ({ e.preventDefault() const isSend = wrappedSendMessage(value, required) if (isSend) { - setValue('') + clearDraft() if (unpinImage) unpinImage() } } @@ -276,7 +307,14 @@ export const ModelInput: FC = ({ const newVal = typeof val === 'function' ? val(value) : val externalOnChange(newVal) } : - setInternalValue + (val: string | ((prev: string) => string)) => { + const next = typeof val === 'function' ? val(value) : val + if (next === '') { + clearDraft() + } else { + setInternalValue(next) + } + } } required={required} />