import { useEffect, useRef, useState } from 'react'; import type { ChatComposerAttachment, ChatMessage } from '../../mainChatPanel/types'; type PreviewItem = { id: string; label: string; url: string; kind: 'image' | 'video' | 'markdown' | 'code' | 'document' | 'pdf' | 'file'; source: 'message' | 'context'; }; type UseConversationViewControllerOptions = { activeSessionId: string; activeView: 'chat' | 'runtime' | 'errors'; previewItems: PreviewItem[]; selectedChatTypeId: string | null; composerRef: { current: { focus: (options?: { cursor?: 'start' | 'end' | 'all' }) => void } | null }; setActiveSystemStatus: (value: string | null) => void; setComposerAttachments: React.Dispatch>; setCopiedMessageId: (value: number | null) => void; setDraft: React.Dispatch>; setIsResourceStripOpen: (value: boolean) => void; setIsSystemStatusPending: (value: boolean) => void; setMessages: React.Dispatch>; }; export function useConversationViewController({ activeSessionId, activeView, composerRef, previewItems, selectedChatTypeId, setActiveSystemStatus, setComposerAttachments, setCopiedMessageId, setDraft, setIsResourceStripOpen, setIsSystemStatusPending, setMessages, }: UseConversationViewControllerOptions) { const previousSessionIdRef = useRef(activeSessionId); const [activePreviewId, setActivePreviewId] = useState(null); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); const [previewText, setPreviewText] = useState(''); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(''); const [previewContentType, setPreviewContentType] = useState(''); const activePreview = previewItems.find((item) => item.id === activePreviewId) ?? previewItems[0] ?? null; useEffect(() => { const hasSessionChanged = previousSessionIdRef.current !== activeSessionId; if (!hasSessionChanged) { return; } previousSessionIdRef.current = activeSessionId; setMessages([]); setDraft(''); setComposerAttachments([]); setCopiedMessageId(null); setActivePreviewId(null); setIsPreviewModalOpen(false); setActiveSystemStatus(null); setIsSystemStatusPending(false); setIsResourceStripOpen(false); }, [ activeSessionId, setActiveSystemStatus, setComposerAttachments, setCopiedMessageId, setDraft, setIsResourceStripOpen, setIsSystemStatusPending, setMessages, ]); useEffect(() => { if (!activePreviewId) { return; } if (previewItems.some((item) => item.id === activePreviewId)) { return; } setActivePreviewId(null); setIsPreviewModalOpen(false); }, [activePreviewId, previewItems]); useEffect(() => { if (!isPreviewModalOpen || !activePreview) { setPreviewText(''); setPreviewError(''); setPreviewContentType(''); setIsPreviewLoading(false); return; } if (activePreview.kind === 'image' || activePreview.kind === 'video' || activePreview.kind === 'pdf') { setPreviewText(''); setPreviewError(''); setPreviewContentType(''); setIsPreviewLoading(false); return; } const controller = new AbortController(); setIsPreviewLoading(true); setPreviewError(''); setPreviewContentType(''); fetch(activePreview.url, { cache: 'no-store', signal: controller.signal, }) .then(async (response) => { if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`); } setPreviewContentType(response.headers.get('content-type') ?? ''); const text = await response.text(); setPreviewText(text.slice(0, 20000)); }) .catch((error: unknown) => { if (controller.signal.aborted) { return; } setPreviewText(''); setPreviewContentType(''); setPreviewError(error instanceof Error ? error.message : 'preview를 가져오지 못했습니다.'); }) .finally(() => { if (!controller.signal.aborted) { setIsPreviewLoading(false); } }); return () => { controller.abort(); }; }, [activePreview, isPreviewModalOpen]); useEffect(() => { if (activeView !== 'chat') { return; } composerRef.current?.focus({ cursor: 'end' }); }, [activeView, composerRef, selectedChatTypeId]); useEffect(() => { if (activeView !== 'chat') { return undefined; } const handleWindowKeyDown = (event: KeyboardEvent) => { const isTextEntryTarget = event.target instanceof HTMLElement && (event.target.isContentEditable || ['input', 'textarea', 'select'].includes(event.target.tagName.toLowerCase()) || event.target.closest('[contenteditable="true"]')); if (event.metaKey || event.ctrlKey || event.altKey || isTextEntryTarget) { return; } if (event.key === '/') { event.preventDefault(); composerRef.current?.focus({ cursor: 'end' }); return; } if (event.key.length === 1) { event.preventDefault(); composerRef.current?.focus({ cursor: 'end' }); setDraft((previous) => previous + event.key); } }; window.addEventListener('keydown', handleWindowKeyDown); return () => { window.removeEventListener('keydown', handleWindowKeyDown); }; }, [activeView, composerRef, setDraft]); return { activePreview, activePreviewId, isPreviewLoading, isPreviewModalOpen, previewContentType, previewError, previewText, setActivePreviewId, setIsPreviewModalOpen, }; }