From ec8ffdf2aeca5ca04c1e4e2e6dfff72d70283d26 Mon Sep 17 00:00:00 2001 From: haesookim Date: Mon, 21 Jul 2025 17:14:08 +0900 Subject: [PATCH] feat: Refactor chat components to unify chat interface modes - Consolidate DefaultChatInterface and NewChatInterface into a single ChatInterface component with mode prop ('existing', 'new-workflow', 'new-default') to handle all chat scenarios. - Introduce ChatHeader, ChatArea, MessageList, EmptyState, and SuggestionChips components to modularize UI and improve readability. - Move type definitions to a dedicated types.tsx file for better maintainability. - Update ChatContent to manage chat modes and workflows consistently. - Enhance ChatInterface to support loading states, error handling, and mode-specific UI. - Simplify chat input area with attachment menu and execution state handling. - Remove unused imports and obsolete components --- src/app/chat/components/ChatArea.tsx | 99 ++++++ src/app/chat/components/ChatContent.tsx | 69 ++-- src/app/chat/components/ChatHeader.tsx | 49 +++ src/app/chat/components/ChatInterface.tsx | 336 +++++++----------- src/app/chat/components/ChatPageContent.tsx | 1 - .../chat/components/CurrentChatInterface.tsx | 1 + src/app/chat/components/EmptyState.tsx | 28 ++ src/app/chat/components/MessageList.tsx | 36 ++ src/app/chat/components/SuggestionChips.tsx | 25 ++ src/app/chat/components/types.tsx | 42 +++ 10 files changed, 442 insertions(+), 244 deletions(-) create mode 100644 src/app/chat/components/ChatArea.tsx create mode 100644 src/app/chat/components/ChatHeader.tsx create mode 100644 src/app/chat/components/EmptyState.tsx create mode 100644 src/app/chat/components/MessageList.tsx create mode 100644 src/app/chat/components/SuggestionChips.tsx create mode 100644 src/app/chat/components/types.tsx diff --git a/src/app/chat/components/ChatArea.tsx b/src/app/chat/components/ChatArea.tsx new file mode 100644 index 00000000..f204f245 --- /dev/null +++ b/src/app/chat/components/ChatArea.tsx @@ -0,0 +1,99 @@ +import { FiClock } from 'react-icons/fi'; +import styles from '@/app/chat/assets/ChatInterface.module.scss'; +import { MessageList } from './MessageList'; +import { EmptyState } from './EmptyState'; +import { IOLog, Workflow } from './types'; + +interface ChatAreaProps { + mode: "existing" | "new-workflow" | "new-default"; + loading: boolean; + ioLogs: IOLog[]; + workflow: Workflow; + executing: boolean; + setInputMessage: (message: string) => void; + messagesRef: React.RefObject; + pendingLogId: string | null; + renderMessageContent: (content: string, isUserMessage?: boolean) => React.ReactNode; + formatDate: (dateString: string) => string; +} + +export const ChatArea: React.FC = ({ + mode, + loading, + ioLogs, + workflow, + executing, + setInputMessage, + messagesRef, + pendingLogId, + renderMessageContent, + formatDate, +}) => { + // 1. 로딩 상태 처리 (existing 모드 전용) + if (mode === 'existing' && loading) { + return ( +
+
+
+

채팅 기록을 불러오는 중...

+
+
+ ); + } + + // 2. 각 모드에 맞는 컨텐츠 렌더링 + const renderContent = () => { + if (mode === 'existing') { + return ioLogs.length === 0 ? ( + +

"{workflow.name}" 워크플로우의 이전 대화를 불러올 수 없습니다.

+

새로운 대화를 시작해보세요.

+
+ ) : ( + + ); + } + + if (mode === 'new-workflow') { + return ( + +

"{workflow.name}" 워크플로우가 준비되었습니다.

+
+ ); + } + + if (mode === 'new-default') { + return ( + +

일반 채팅 모드로 자유롭게 대화할 수 있습니다.

+
+ ); + } + + return null; // 예외 케이스 + }; + + return ( +
+
+ {renderContent()} +
+
+ ); +}; \ No newline at end of file diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx index 336ab979..0ec84ced 100644 --- a/src/app/chat/components/ChatContent.tsx +++ b/src/app/chat/components/ChatContent.tsx @@ -5,8 +5,6 @@ import { LuWorkflow } from "react-icons/lu"; import { IoChatbubblesOutline } from "react-icons/io5"; import WorkflowSelection from './WorkflowSelection'; import ChatInterface from './ChatInterface'; -import NewChatInterface from './NewChatInterface'; -import DefaultChatInterface from './DefaultChatInterface'; interface ChatContentProps { onChatStarted?: () => void; // 채팅 시작 후 호출될 콜백 @@ -49,57 +47,46 @@ const ChatContentInner: React.FC = ({ onChatStarted }) => { const handleWorkflowSelect = (workflow: any) => { setSelectedWorkflow(workflow); - // 새로운 채팅으로 시작 (항상 NewChatInterface 사용) setCurrentView('newChat'); }; const handleDefaultChatStart = () => { + setSelectedWorkflow({ + id: 'default_mode', + name: 'default_mode', + filename: 'default_chat', + author: 'System', + nodeCount: 1, + status: 'active' as const, + }); setCurrentView('defaultChat'); }; - // 일반 채팅 화면 (DefaultChatInterface) - if (currentView === 'defaultChat') { - return ( -
-
- setCurrentView('welcome')} - onChatStarted={onChatStarted} - /> -
-
- ); - } + const getChatMode = () => { + if (currentView === 'existingChat' && selectedWorkflow) return 'existing'; + if (currentView === 'newChat'&& selectedWorkflow) return 'new-workflow'; + if (currentView === 'defaultChat') return 'new-default'; + return null; + }; - // 새로운 채팅 화면 (NewChatInterface) - if (currentView === 'newChat' && selectedWorkflow) { - return ( -
-
- setCurrentView('workflow')} - onChatStarted={onChatStarted} - /> -
-
- ); - } + const chatMode = getChatMode(); - // 기존 채팅 화면 (ChatInterface) - if (currentView === 'existingChat' && selectedWorkflow) { + if (chatMode) { return ( -
-
- setCurrentView('workflow')} - /> -
+
+ {chatMode && ( + setCurrentView('welcome') : () => setCurrentView('workflow')} + /> + )}
); - } + }; // 워크플로우 선택 화면 if (currentView === 'workflow') { diff --git a/src/app/chat/components/ChatHeader.tsx b/src/app/chat/components/ChatHeader.tsx new file mode 100644 index 00000000..7e268536 --- /dev/null +++ b/src/app/chat/components/ChatHeader.tsx @@ -0,0 +1,49 @@ +import { FiArrowLeft, FiMessageSquare } from 'react-icons/fi'; +import styles from '@/app/chat/assets/ChatInterface.module.scss'; +import { ChatHeaderProps } from './types'; + +const ChatHeader: React.FC = ({ mode, workflow, ioLogs, onBack, hideBackButton }) => { + let title = ''; + let subtitle = ''; + let chatCountText = ''; + + const isExistingMode = mode === 'existing'; + + if (isExistingMode) { + title = workflow.name === 'default_mode' ? '일반 채팅' : workflow.name; + subtitle = hideBackButton ? '현재 채팅을 계속하세요' : '기존 대화를 계속하세요'; + chatCountText = `${ioLogs.length}개의 대화`; + } else if (mode === 'new-default') { + title = '일반 채팅'; + subtitle = '자유롭게 대화를 시작하세요'; + chatCountText = '새 채팅'; + } else { + title = workflow.name; + subtitle = '새로운 대화를 시작하세요'; + chatCountText = '새 채팅'; + } + + const showBackButton = (!isExistingMode || !hideBackButton); + + return ( +
+
+ {showBackButton && ( + + )} +
+

{title}

+

{subtitle}

+
+
+
+ + {chatCountText} +
+
+ ); +}; + +export default ChatHeader; \ No newline at end of file diff --git a/src/app/chat/components/ChatInterface.tsx b/src/app/chat/components/ChatInterface.tsx index 8eda9479..6ef0d962 100644 --- a/src/app/chat/components/ChatInterface.tsx +++ b/src/app/chat/components/ChatInterface.tsx @@ -17,43 +17,14 @@ import { getWorkflowIOLogs, executeWorkflowById } from '@/app/api/workflowAPI'; import { MessageRenderer } from '@/app/utils/chatParser'; import toast from 'react-hot-toast'; import CollectionModal from '@/app/chat/components/CollectionModal'; +import { IOLog, ChatInterfaceProps } from './types'; +import ChatHeader from './ChatHeader'; +import { ChatArea } from './ChatArea'; -interface Workflow { - id: string; - name: string; - description?: string; - createdAt?: string; - lastModified?: string; - author: string; - nodeCount: number; - status: 'active' | 'draft' | 'archived'; - filename?: string; - error?: string; -} - -interface IOLog { - log_id: number | string; - workflow_name: string; - workflow_id: string; - input_data: string; - output_data: string; - updated_at: string; -} - -interface ChatInterfaceProps { - workflow: Workflow; - onBack: () => void; - hideBackButton?: boolean; - existingChatData?: { - interactionId: string; - workflowId: string; - workflowName: string; - } | null; -} - -const ChatInterface: React.FC = ({ workflow, onBack, hideBackButton = false, existingChatData }) => { + +const ChatInterface: React.FC = ({ mode, workflow, onBack, hideBackButton = false, existingChatData }) => { const [ioLogs, setIOLogs] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [executing, setExecuting] = useState(false); const [error, setError] = useState(null); const [inputMessage, setInputMessage] = useState(''); @@ -66,10 +37,29 @@ const ChatInterface: React.FC = ({ workflow, onBack, hideBac const attachmentButtonRef = useRef(null); useEffect(() => { - if (workflow?.id && existingChatData?.interactionId) { + const loadChatLogs = async () => { + try { + setLoading(true); + setError(null); + + const interactionId = existingChatData?.interactionId || 'default'; + const workflowName = existingChatData?.workflowName || workflow.name; + const workflowId = existingChatData?.workflowId || workflow.id; + + const logs = await getWorkflowIOLogs(workflowName, workflowId, interactionId); + setIOLogs((logs as any).in_out_logs || []); + setPendingLogId(null); + } catch (err) { + setError('채팅 기록을 불러오는데 실패했습니다.'); + setIOLogs([]); + } finally { + setLoading(false); + } + }; + if (mode === 'existing' && workflow?.id && existingChatData?.interactionId) { loadChatLogs(); } - }, [workflow?.id, existingChatData?.interactionId, existingChatData?.workflowId]); + }, [mode, existingChatData?.interactionId, workflow.id, workflow.name, existingChatData?.workflowName, existingChatData?.workflowId]); useEffect(() => { scrollToBottom(); @@ -133,26 +123,6 @@ const ChatInterface: React.FC = ({ workflow, onBack, hideBac } }; - const loadChatLogs = async () => { - try { - setLoading(true); - setError(null); - - const interactionId = existingChatData?.interactionId || 'default'; - const workflowName = existingChatData?.workflowName || workflow.name; - const workflowId = existingChatData?.workflowId || workflow.id; - - const logs = await getWorkflowIOLogs(workflowName, workflowId, interactionId); - setIOLogs((logs as any).in_out_logs || []); - setPendingLogId(null); - } catch (err) { - setError('채팅 기록을 불러오는데 실패했습니다.'); - setIOLogs([]); - } finally { - setLoading(false); - } - }; - const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString('ko-KR', { month: 'short', @@ -308,166 +278,128 @@ const ChatInterface: React.FC = ({ workflow, onBack, hideBac return (
- {/* Header */} -
-
- {!hideBackButton && ( - - )} -
-

{workflow.name === 'default_mode' ? '일반 채팅' : workflow.name}

-

{hideBackButton ? '현재 채팅을 계속하세요' : '기존 대화를 계속하세요'}

-
-
-
- - {ioLogs.length}개의 대화 -
-
+ {/* Chat Area */}
- {loading ? ( -
-
-

채팅 기록을 불러오는 중...

-
- ) : ( - <> -
- {ioLogs.length === 0 ? ( -
- -

대화 기록이 없습니다

-

"{workflow.name}" 워크플로우의 이전 대화를 불러올 수 없습니다.

-

새로운 대화를 시작해보세요.

-
- ) : ( - ioLogs.map((log) => ( -
- {/* User Message */} -
-
- {renderMessageContent(log.input_data, true)} -
-
- {formatDate(log.updated_at)} -
-
- - {/* Bot Message */} -
-
- {String(log.log_id) === pendingLogId && executing && !log.output_data ? ( -
- - - -
- ) : ( - renderMessageContent(log.output_data) - )} -
-
-
- )) - )} -
- - - {/* Input Area */} -
-
- setInputMessage(e.target.value)} - onKeyPress={handleKeyPress} - disabled={executing} - className={styles.messageInput} - /> -
- {selectedCollection && ( -
- - {selectedCollection} - -
- )} -
+ {/* Chat Area */} + + + <> + {/* Input Area */} +
+
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={executing} + className={styles.messageInput} + /> +
+ {selectedCollection && ( +
+ + {selectedCollection} - {showAttachmentMenu && ( -
- - - - -
- )}
+ )} +
+ {showAttachmentMenu && ( +
+ + + + +
+ )}
+
- {executing && ( +
+ {executing && ( + mode === "new-default" ? ( +

+ 일반 채팅을 실행 중입니다... +

+ ) : (

워크플로우를 실행 중입니다...

- )} - {error && ( -

{error}

- )} -
- - )} + ) + + )} + {error && ( +

{error}

+ )} +
+
{/* Collection Modal */} diff --git a/src/app/chat/components/ChatPageContent.tsx b/src/app/chat/components/ChatPageContent.tsx index ad31f23e..6745d828 100644 --- a/src/app/chat/components/ChatPageContent.tsx +++ b/src/app/chat/components/ChatPageContent.tsx @@ -4,7 +4,6 @@ import Sidebar from '@/app/_common/components/Sidebar'; import ChatHistory from '@/app/chat/components/ChatHistory'; import CurrentChatInterface from '@/app/chat/components/CurrentChatInterface'; import ChatContent from '@/app/chat/components/ChatContent'; -import DefaultChatInterface from '@/app/chat/components/DefaultChatInterface'; import { getChatSidebarItems, getSettingSidebarItems, createItemClickHandler } from '@/app/_common/components/sidebarConfig'; import styles from '@/app/main/assets/MainPage.module.scss'; import { useSearchParams, useRouter, usePathname } from 'next/navigation'; diff --git a/src/app/chat/components/CurrentChatInterface.tsx b/src/app/chat/components/CurrentChatInterface.tsx index 2ccc7ab1..22d22b6e 100644 --- a/src/app/chat/components/CurrentChatInterface.tsx +++ b/src/app/chat/components/CurrentChatInterface.tsx @@ -83,6 +83,7 @@ const CurrentChatInterface: React.FC = ({ onBack }) =
void; + disabled?: boolean; +} +export const EmptyState: React.FC = ({ icon, title, children, showSuggestions, onChipClick, disabled }) => { + return ( +
+ {icon || } +

{title}

+ {children} + {showSuggestions && ( +
+ +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/app/chat/components/MessageList.tsx b/src/app/chat/components/MessageList.tsx new file mode 100644 index 00000000..3df61fb3 --- /dev/null +++ b/src/app/chat/components/MessageList.tsx @@ -0,0 +1,36 @@ +import styles from '@/app/chat/assets/ChatInterface.module.scss'; +import { IOLog } from './types'; + +interface MessageListProps { + ioLogs: IOLog[]; + pendingLogId: string | null; + executing: boolean; + renderMessageContent: (content: string, isUserMessage?: boolean) => React.ReactNode; + formatDate: (dateString: string) => string; +} +export const MessageList: React.FC = ({ ioLogs, pendingLogId, executing, renderMessageContent, formatDate }) => { + return ioLogs.map((log) => ( +
+
+
+ {renderMessageContent(log.input_data, true)} +
+
+ {formatDate(log.updated_at)} +
+
+ +
+
+ {String(log.log_id) === pendingLogId && executing && !log.output_data ? ( +
+ +
+ ) : ( + renderMessageContent(log.output_data) + )} +
+
+
+ )); +}; \ No newline at end of file diff --git a/src/app/chat/components/SuggestionChips.tsx b/src/app/chat/components/SuggestionChips.tsx new file mode 100644 index 00000000..486959e5 --- /dev/null +++ b/src/app/chat/components/SuggestionChips.tsx @@ -0,0 +1,25 @@ +import styles from '@/app/chat/assets/ChatInterface.module.scss'; + +interface SuggestionChipsProps { + onChipClick?: (text: string) => void; + disabled?: boolean; +} + +export const SuggestionChips: React.FC = ({ onChipClick, disabled }) => { + const suggestions = ['안녕하세요!', '도움이 필요해요', '어떤 기능이 있나요?']; + + return ( +
+ {suggestions.map((text) => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/src/app/chat/components/types.tsx b/src/app/chat/components/types.tsx new file mode 100644 index 00000000..fc3ce8a0 --- /dev/null +++ b/src/app/chat/components/types.tsx @@ -0,0 +1,42 @@ +export interface Workflow { + id: string; + name: string; + description?: string; + createdAt?: string; + lastModified?: string; + author: string; + nodeCount: number; + status: 'active' | 'draft' | 'archived'; + filename?: string; + error?: string; +} + +export interface IOLog { + log_id: number | string; + workflow_name: string; + workflow_id: string; + input_data: string; + output_data: string; + updated_at: string; +} + +export interface ChatInterfaceProps { + mode: 'existing' | 'new-workflow' | 'new-default'; + workflow: Workflow; + onChatStarted?: (newInteractionId: string) => void; + onBack: () => void; + hideBackButton?: boolean; + existingChatData?: { + interactionId: string; + workflowId: string; + workflowName: string; + } | null; +} + +export interface ChatHeaderProps { + mode: 'existing' | 'new-workflow' | 'new-default'; + workflow: Workflow; + ioLogs: IOLog[]; + onBack: () => void; + hideBackButton?: boolean; +} \ No newline at end of file