204 lines
5.7 KiB
TypeScript
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,
|
|
};
|
|
}
|