Files
ai-code-app/src/app/main/chatV2/hooks/useConversationViewController.ts

204 lines
5.7 KiB
TypeScript

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<React.SetStateAction<ChatComposerAttachment[]>>;
setCopiedMessageId: (value: number | null) => void;
setDraft: React.Dispatch<React.SetStateAction<string>>;
setIsResourceStripOpen: (value: boolean) => void;
setIsSystemStatusPending: (value: boolean) => void;
setMessages: React.Dispatch<React.SetStateAction<ChatMessage[]>>;
};
export function useConversationViewController({
activeSessionId,
activeView,
composerRef,
previewItems,
selectedChatTypeId,
setActiveSystemStatus,
setComposerAttachments,
setCopiedMessageId,
setDraft,
setIsResourceStripOpen,
setIsSystemStatusPending,
setMessages,
}: UseConversationViewControllerOptions) {
const previousSessionIdRef = useRef(activeSessionId);
const [activePreviewId, setActivePreviewId] = useState<string | null>(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,
};
}