diff --git a/src/app/main/components/Sidebar.tsx b/src/app/_common/components/Sidebar.tsx similarity index 56% rename from src/app/main/components/Sidebar.tsx rename to src/app/_common/components/Sidebar.tsx index ef173846..12f2b8fb 100644 --- a/src/app/main/components/Sidebar.tsx +++ b/src/app/_common/components/Sidebar.tsx @@ -1,13 +1,12 @@ 'use client'; import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { RiChatSmileAiLine } from "react-icons/ri"; -import { FiClock, FiMessageCircle } from "react-icons/fi"; +import { useRouter, usePathname } from 'next/navigation'; import { SidebarProps } from '@/app/main/components/types'; import styles from '@/app/main/assets/MainPage.module.scss'; const Sidebar: React.FC = ({ items, + chatItems = [], activeItem, onItemClick, className = '', @@ -15,6 +14,7 @@ const Sidebar: React.FC = ({ initialSettingExpanded = false, }) => { const router = useRouter(); + const pathname = usePathname(); const [isSettingExpanded, setIsSettingExpanded] = useState(initialSettingExpanded); const [isChatExpanded, setIsChatExpanded] = useState(initialChatExpanded); @@ -36,19 +36,13 @@ const Sidebar: React.FC = ({ router.push('/'); }; - const handleNewChatClick = () => { - onItemClick('new-chat'); - router.push('/chat'); - }; - - const handleChatHistoryClick = () => { - onItemClick('chat-history'); - router.push('/chat'); - }; - - const handleCurrentChatClick = () => { - onItemClick('current-chat'); - router.push('/chat'); + const handleChatItemClick = (itemId: string) => { + onItemClick(itemId); + // /chat 페이지가 아닌 경우에만 localStorage에 저장하고 라우팅 + if (pathname !== '/chat') { + localStorage.setItem('activeChatSection', itemId); + router.push('/chat'); + } }; return ( @@ -74,44 +68,21 @@ const Sidebar: React.FC = ({ {isChatExpanded && ( )} diff --git a/src/app/_common/components/sidebarConfig.ts b/src/app/_common/components/sidebarConfig.ts index fe176c64..3c342648 100644 --- a/src/app/_common/components/sidebarConfig.ts +++ b/src/app/_common/components/sidebarConfig.ts @@ -5,12 +5,35 @@ import { FiCpu, FiSettings, FiEye, + FiClock, + FiMessageCircle, FiFile, } from 'react-icons/fi'; +import { RiChatSmileAiLine } from "react-icons/ri"; import { SidebarItem } from '@/app/main/components/types'; -// 워크플로우 관리 센터의 공통 사이드바 아이템들을 반환하는 함수 -export const getSidebarItems = (): SidebarItem[] => [ +export const getChatSidebarItems = (): SidebarItem[] => [ + { + id: 'new-chat', + title: '새 채팅', + description: '새로운 AI 채팅을 시작합니다', + icon: React.createElement(RiChatSmileAiLine), + }, + { + id: 'current-chat', + title: '현재 채팅', + description: '진행 중인 대화를 계속합니다', + icon: React.createElement(FiMessageCircle), + }, + { + id: 'chat-history', + title: '기존 채팅 불러오기', + description: '이전 대화를 불러와서 계속합니다', + icon: React.createElement(FiClock), + }, +]; + +export const getSettingSidebarItems = (): SidebarItem[] => [ { id: 'canvas', title: '워크플로우 캔버스', @@ -57,3 +80,12 @@ export const createItemClickHandler = (router: any) => { router.push('/main'); }; }; + +// 채팅 아이템 클릭 핸들러 (localStorage 사용) +export const createChatItemClickHandler = (router: any) => { + return (itemId: string) => { + // 클릭한 채팅 섹션을 localStorage에 저장하고 /chat으로 이동 + localStorage.setItem('activeChatSection', itemId); + router.push('/chat'); + }; +}; diff --git a/src/app/api/chatAPI.js b/src/app/api/chatAPI.js index ff999f40..9abd5e03 100644 --- a/src/app/api/chatAPI.js +++ b/src/app/api/chatAPI.js @@ -1,9 +1,142 @@ +// Configuration API 호출 함수들을 관리하는 파일 +import { devLog } from '@/app/utils/logger'; +import { API_BASE_URL } from '@/app/config.js'; + /** - * Simulates sending a message to a chat API and receiving a response. - * @param {string} message - The message to send. - * @returns {Promise<{text: string}>} A promise that resolves with a response object. + * Creates a new chat session + * @param {Object} params - The chat creation parameters + * @param {string} params.interaction_id - Unique interaction identifier + * @param {string} [params.input_data] - Optional initial message + * @returns {Promise} A promise that resolves with the chat creation response */ -export const sendMessage = (message) => { +export const createNewChat = async ({ interaction_id, input_data = null }) => { + try { + const response = await fetch(`${API_BASE_URL}/api/chat/new`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workflow_name: "default_mode", + workflow_id: "default_mode", + interaction_id, + input_data + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP ${response.status}: ${errorData.detail || 'Unknown error'}`); + } + + return await response.json(); + } catch (error) { + console.error('Error creating new chat:', error); + throw error; + } +}; + +/** + * Continues an existing chat session + * @param {Object} params - The chat execution parameters + * @param {string} params.user_input - The user's message + * @param {string} params.interaction_id - The interaction identifier + * @param {string} [params.workflow_id] - Optional workflow ID (defaults to "default_mode") + * @param {string} [params.workflow_name] - Optional workflow name (defaults to "default_mode") + * @returns {Promise} A promise that resolves with the chat response + */ +export const executeChatMessage = async ({ + user_input, + interaction_id, + workflow_id = "default_mode", + workflow_name = "default_mode" +}) => { + try { + const response = await fetch(`${API_BASE_URL}/api/chat/execution`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + user_input, + interaction_id, + workflow_id, + workflow_name + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`HTTP ${response.status}: ${errorData.detail || 'Unknown error'}`); + } + + return await response.json(); + } catch (error) { + console.error('Error executing chat message:', error); + throw error; + } +}; + +/** + * High-level function to handle a complete chat flow + * @param {Object} params - The chat parameters + * @param {string} params.message - The user's message + * @param {string} [params.interaction_id] - Existing interaction ID or will generate new one + * @param {boolean} [params.isNewChat] - Whether this is a new chat session + * @returns {Promise} A promise that resolves with the chat response + */ +export const sendMessage = async ({ + message, + interaction_id = null, + isNewChat = false +}) => { + try { + // Generate interaction ID if not provided + const chatInteractionId = interaction_id || `chat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + if (isNewChat) { + // Create new chat session with initial message + const result = await createNewChat({ + interaction_id: chatInteractionId, + input_data: message + }); + + return { + text: result.chat_response || 'Chat session created successfully', + interaction_id: result.interaction_id, + session_info: result.execution_meta, + timestamp: result.timestamp, + status: result.status + }; + } else { + // Continue existing chat session + const result = await executeChatMessage({ + user_input: message, + interaction_id: chatInteractionId + }); + + return { + text: result.ai_response, + interaction_id: result.interaction_id, + session_id: result.session_id, + session_info: result.execution_meta, + timestamp: result.timestamp, + status: result.status + }; + } + } catch (error) { + console.error('Error in sendMessage:', error); + throw error; + } +}; + +/** + * Legacy function for backward compatibility - simulates sending a message + * @deprecated Use sendMessage with proper parameters instead + * @param {string} message - The message to send + * @returns {Promise<{text: string}>} A promise that resolves with a response object + */ +export const sendMessageLegacy = (message) => { return new Promise((resolve) => { setTimeout(() => { resolve({ diff --git a/src/app/api/interactionAPI.js b/src/app/api/interactionAPI.js index 9846a990..6320731f 100644 --- a/src/app/api/interactionAPI.js +++ b/src/app/api/interactionAPI.js @@ -21,7 +21,7 @@ export const listInteractions = async (filters = {}) => { params.append('limit', limit.toString()); const response = await fetch( - `${API_BASE_URL}/interaction/list?${params}`, + `${API_BASE_URL}/api/interaction/list?${params}`, { method: 'GET', headers: { @@ -74,7 +74,7 @@ export const executeWorkflowNew = async (requestData) => { devLog.log('Executing new workflow with data:', requestBody); - const response = await fetch(`${API_BASE_URL}/interaction/new`, { + const response = await fetch(`${API_BASE_URL}/api/interaction/new`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/api/nodeAPI.js b/src/app/api/nodeAPI.js index 4107edf1..7ce28e54 100644 --- a/src/app/api/nodeAPI.js +++ b/src/app/api/nodeAPI.js @@ -7,7 +7,7 @@ import { API_BASE_URL } from '@/app/config.js'; */ export const getNodes = async () => { try { - const response = await fetch(`${API_BASE_URL}/node/get`); + const response = await fetch(`${API_BASE_URL}/api/node/get`); if (!response.ok) { const errorData = await response.json(); @@ -30,7 +30,7 @@ export const getNodes = async () => { */ export const exportNodes = async () => { try { - const response = await fetch(`${API_BASE_URL}/node/export`); + const response = await fetch(`${API_BASE_URL}/api/node/export`); if (!response.ok) { const errorData = await response.json(); diff --git a/src/app/api/workflowAPI.js b/src/app/api/workflowAPI.js index 3964345d..323bcd5d 100644 --- a/src/app/api/workflowAPI.js +++ b/src/app/api/workflowAPI.js @@ -9,7 +9,7 @@ import { API_BASE_URL } from '@/app/config.js'; */ export const executeWorkflow = async (workflowData) => { try { - const response = await fetch(`${API_BASE_URL}/workflow/execute`, { + const response = await fetch(`${API_BASE_URL}/api/workflow/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -51,7 +51,7 @@ export const saveWorkflow = async (workflowId, workflowContent) => { Object.keys(workflowContent), ); - const response = await fetch(`${API_BASE_URL}/workflow/save`, { + const response = await fetch(`${API_BASE_URL}/api/workflow/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -84,7 +84,7 @@ export const saveWorkflow = async (workflowId, workflowContent) => { */ export const listWorkflows = async () => { try { - const response = await fetch(`${API_BASE_URL}/workflow/list`); + const response = await fetch(`${API_BASE_URL}/api/workflow/list`); if (!response.ok) { const errorData = await response.json(); @@ -108,7 +108,7 @@ export const listWorkflows = async () => { */ export const listWorkflowsDetail = async () => { try { - const response = await fetch(`${API_BASE_URL}/workflow/list/detail`); + const response = await fetch(`${API_BASE_URL}/api/workflow/list/detail`); if (!response.ok) { const errorData = await response.json(); @@ -139,7 +139,7 @@ export const loadWorkflow = async (workflowId) => { : workflowId; const response = await fetch( - `${API_BASE_URL}/workflow/load/${encodeURIComponent(cleanWorkflowId)}`, + `${API_BASE_URL}/api/workflow/load/${encodeURIComponent(cleanWorkflowId)}`, ); if (!response.ok) { @@ -166,7 +166,7 @@ export const loadWorkflow = async (workflowId) => { export const deleteWorkflow = async (workflowId) => { try { const response = await fetch( - `${API_BASE_URL}/workflow/delete/${encodeURIComponent(workflowId)}`, + `${API_BASE_URL}/api/workflow/delete/${encodeURIComponent(workflowId)}`, { method: 'DELETE', }, @@ -194,7 +194,7 @@ export const deleteWorkflow = async (workflowId) => { */ export const getWorkflowList = async () => { try { - const response = await fetch(`${API_BASE_URL}/workflow/list/detail`, { + const response = await fetch(`${API_BASE_URL}/api/workflow/list/detail`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -232,7 +232,7 @@ export const getWorkflowPerformance = async (workflowName, workflowId) => { }); const response = await fetch( - `${API_BASE_URL}/workflow/performance?${params}`, + `${API_BASE_URL}/api/workflow/performance?${params}`, { method: 'GET', headers: { @@ -274,7 +274,7 @@ export const getWorkflowIOLogs = async (workflowName, workflowId, interactionId }); const response = await fetch( - `${API_BASE_URL}/workflow/io_logs?${params}`, + `${API_BASE_URL}/api/workflow/io_logs?${params}`, { method: 'GET', headers: { @@ -316,7 +316,7 @@ export const deleteWorkflowIOLogs = async (workflowName, workflowId, interaction }); const response = await fetch( - `${API_BASE_URL}/workflow/io_logs?${params}`, + `${API_BASE_URL}/api/workflow/io_logs?${params}`, { method: 'DELETE', headers: { @@ -363,7 +363,7 @@ export const executeWorkflowById = async ( interaction_id: interaction_id || 'default', }; const response = await fetch( - `${API_BASE_URL}/workflow/execute/based_id`, + `${API_BASE_URL}/api/workflow/execute/based_id`, { method: 'POST', headers: { @@ -404,7 +404,7 @@ export const deleteWorkflowPerformance = async (workflowName, workflowId) => { }); const response = await fetch( - `${API_BASE_URL}/workflow/performance?${params}`, + `${API_BASE_URL}/api/workflow/performance?${params}`, { method: 'DELETE', headers: { diff --git a/src/app/canvas/components/SideMenuPanel/ChatPanel.tsx b/src/app/canvas/components/SideMenuPanel/ChatPanel.tsx index b2de0547..2630e15f 100644 --- a/src/app/canvas/components/SideMenuPanel/ChatPanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/ChatPanel.tsx @@ -40,7 +40,10 @@ const ChatPanel: React.FC = ({ onBack }) => { setInputValue(''); try { - const response: SendMessageResponse = await sendMessage(userMessage.text); + const response = await sendMessage({ + message: userMessage.text, + isNewChat: messages.length === 1 // 첫 번째 메시지면 새 채팅 + }) as SendMessageResponse; const botMessage: Message = { id: Date.now() + 1, text: response.text, diff --git a/src/app/canvas/types.ts b/src/app/canvas/types.ts index 1f66bb9a..e4bddc03 100644 --- a/src/app/canvas/types.ts +++ b/src/app/canvas/types.ts @@ -295,6 +295,11 @@ export interface ChatPanelProps { export interface SendMessageResponse { text: string; + interaction_id?: string; + session_id?: string; + session_info?: any; + timestamp?: string; + status?: string; } // ========== Utility Types ========== diff --git a/src/app/chat/assets/ChatInterface.module.scss b/src/app/chat/assets/ChatInterface.module.scss index 566ea17f..921ad0d9 100644 --- a/src/app/chat/assets/ChatInterface.module.scss +++ b/src/app/chat/assets/ChatInterface.module.scss @@ -424,6 +424,164 @@ $white: #ffffff; } } +// Welcome Message for DefaultChatInterface +.welcomeMessage { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + color: $gray-500; + padding: 2rem; + + .welcomeIcon { + width: 4rem; + height: 4rem; + margin-bottom: 1.5rem; + color: $primary-blue; + opacity: 0.6; + } + + h3 { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.75rem 0; + color: $gray-700; + } + + p { + margin: 0; + line-height: 1.6; + font-size: 1rem; + color: $gray-500; + } +} + +// Message Group for Chat Messages +.messageGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +// AI Message styles +.aiMessage { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: 1rem; + + .messageContent { + background: $white; + border: 1px solid $gray-200; + color: $gray-900; + padding: 0.75rem 1rem; + border-radius: 1rem 1rem 1rem 0.25rem; + max-width: 70%; + word-wrap: break-word; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + line-height: 1.5; + } + + .messageTime { + font-size: 0.75rem; + color: $gray-500; + margin-top: 0.25rem; + margin-left: 0.25rem; + } +} + +// Loading dots animation +.loadingDots { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0; + + span { + width: 6px; + height: 6px; + background-color: $gray-400; + border-radius: 50%; + animation: loadingDots 1.4s infinite ease-in-out; + + &:nth-child(1) { + animation-delay: -0.32s; + } + + &:nth-child(2) { + animation-delay: -0.16s; + } + + &:nth-child(3) { + animation-delay: 0s; + } + } +} + +@keyframes loadingDots { + 0%, + 80%, + 100% { + transform: scale(0.8); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +// Error message styles +.errorMessage { + background: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + margin: 1rem 0; + font-size: 0.875rem; + font-weight: 500; +} + +// Chat count styles +.chatCount { + display: flex; + align-items: center; + gap: 0.5rem; + color: $gray-600; + font-size: 0.875rem; + + svg { + font-size: 1rem; + } +} + +// Back button styles +.backButton { + background: none; + border: none; + color: $gray-600; + cursor: pointer; + padding: 0.5rem; + border-radius: 0.5rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: $gray-100; + color: $gray-900; + } + + svg { + font-size: 1.25rem; + } +} + // Responsive Design @media (max-width: 768px) { .header { diff --git a/src/app/chat/assets/WorkflowSelection.module.scss b/src/app/chat/assets/WorkflowSelection.module.scss index 953361af..3463d487 100644 --- a/src/app/chat/assets/WorkflowSelection.module.scss +++ b/src/app/chat/assets/WorkflowSelection.module.scss @@ -55,28 +55,24 @@ $white: #ffffff; } .backButton { - background: $white; - border: 2px solid $gray-200; - border-radius: 0.5rem; - padding: 0.75rem; + background: none; + border: none; + color: $gray-600; cursor: pointer; + padding: 0.5rem; + border-radius: 0.5rem; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; - svg { - font-size: 1.25rem; - color: $gray-600; - } - &:hover { - border-color: $primary-blue; - background: $gray-50; + background: $gray-100; + color: $gray-900; + } - svg { - color: $primary-blue; - } + svg { + font-size: 1.25rem; } } diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx index e88cbab6..00629e1c 100644 --- a/src/app/chat/components/ChatContent.tsx +++ b/src/app/chat/components/ChatContent.tsx @@ -6,10 +6,11 @@ import { IoChatbubblesOutline } from "react-icons/io5"; import WorkflowSelection from './WorkflowSelection'; import ChatInterface from './ChatInterface'; import NewChatInterface from './NewChatInterface'; +import DefaultChatInterface from './DefaultChatInterface'; const ChatContentInner: React.FC = () => { const searchParams = useSearchParams(); - const [currentView, setCurrentView] = useState<'welcome' | 'workflow' | 'newChat' | 'existingChat'>('welcome'); + const [currentView, setCurrentView] = useState<'welcome' | 'workflow' | 'newChat' | 'existingChat' | 'defaultChat'>('welcome'); const [selectedWorkflow, setSelectedWorkflow] = useState(null); const [existingChatData, setExistingChatData] = useState(null); @@ -48,6 +49,23 @@ const ChatContentInner: React.FC = () => { setCurrentView('newChat'); }; + const handleDefaultChatStart = () => { + setCurrentView('defaultChat'); + }; + + // 일반 채팅 화면 (DefaultChatInterface) + if (currentView === 'defaultChat') { + return ( +
+
+ setCurrentView('welcome')} + /> +
+
+ ); + } + // 새로운 채팅 화면 (NewChatInterface) if (currentView === 'newChat' && selectedWorkflow) { return ( @@ -107,7 +125,10 @@ const ChatContentInner: React.FC = () => {

Workflow 선택

정해진 워크플로우로 시작하기

- + {!hideBackButton && ( + + )}

{workflow.name}

-

기존 대화를 계속하세요

+

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

diff --git a/src/app/chat/components/ChatPageContent.tsx b/src/app/chat/components/ChatPageContent.tsx new file mode 100644 index 00000000..f7b6b5ee --- /dev/null +++ b/src/app/chat/components/ChatPageContent.tsx @@ -0,0 +1,105 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +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'; + +const ChatPageContent: React.FC = () => { + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + const [activeSection, setActiveSection] = useState('new-chat'); + const [initialLoad, setInitialLoad] = useState(true); + + useEffect(() => { + const mode = searchParams.get('mode'); + const interactionId = searchParams.get('interaction_id'); + + if (mode === 'current') { + setActiveSection('current-chat'); + } else if (mode === 'existing' && interactionId) { + setActiveSection('new-chat'); + } else if (mode === 'history') { + setActiveSection('chat-history'); + } else { + // localStorage에서 저장된 activeChatSection 확인 + const savedActiveChatSection = localStorage.getItem('activeChatSection'); + if (savedActiveChatSection && ['new-chat', 'current-chat', 'chat-history'].includes(savedActiveChatSection)) { + setActiveSection(savedActiveChatSection); + localStorage.removeItem('activeChatSection'); + } + // 일반적인 activeSection도 확인 (기존 호환성 유지) + else { + const savedActiveSection = localStorage.getItem('activeSection'); + if (savedActiveSection && ['new-chat', 'current-chat', 'chat-history'].includes(savedActiveSection)) { + setActiveSection(savedActiveSection); + localStorage.removeItem('activeSection'); + } + } + } + setInitialLoad(false); + }, [searchParams]); + + useEffect(() => { + if (!initialLoad) { + const currentParams = new URLSearchParams(searchParams.toString()); + if (currentParams.has('mode') || currentParams.has('interaction_id')) { + router.replace(pathname); + } + } + }, [activeSection, initialLoad, pathname, router, searchParams]); + + const handleSidebarItemClick = (id: string) => { + // 채팅 관련 아이템인지 확인 + const chatItems = ['new-chat', 'current-chat', 'chat-history']; + if (chatItems.includes(id)) { + setActiveSection(id); + } else { + // 다른 섹션으로 이동 (localStorage에 저장 후 /main으로 이동) + const handleItemClick = createItemClickHandler(router); + handleItemClick(id); + } + }; + + const handleChatSelect = (executionMeta: any) => { + console.log('Selected chat:', executionMeta); + setActiveSection('current-chat'); + }; + + const settingSidebarItems = getSettingSidebarItems(); + const chatSidebarItems = getChatSidebarItems(); + + const renderContent = () => { + switch (activeSection) { + case 'new-chat': + return + case 'current-chat': + return ; + case 'chat-history': + return + default: + return + } + }; + + return ( +
+ +
{renderContent()}
+
+ ); +}; + +export default ChatPageContent; diff --git a/src/app/chat/components/CurrentChatInterface.tsx b/src/app/chat/components/CurrentChatInterface.tsx index f1074802..2ccc7ab1 100644 --- a/src/app/chat/components/CurrentChatInterface.tsx +++ b/src/app/chat/components/CurrentChatInterface.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import ChatInterface from './ChatInterface'; import { FiMessageSquare } from 'react-icons/fi'; import styles from '@/app/chat/assets/ChatInterface.module.scss'; @@ -14,7 +14,6 @@ const CurrentChatInterface: React.FC = ({ onBack }) = const [loading, setLoading] = useState(true); useEffect(() => { - // localStorage에서 현재 채팅 정보 가져오기 const savedChatData = localStorage.getItem('currentChatData'); if (savedChatData) { try { @@ -27,6 +26,28 @@ const CurrentChatInterface: React.FC = ({ onBack }) = setLoading(false); }, []); + // Hook들은 항상 같은 순서로 호출되어야 함 + const workflow = useMemo(() => { + if (!currentChatData) return null; + return { + id: currentChatData.workflowId, + name: currentChatData.workflowName, + filename: currentChatData.workflowName, + author: 'Unknown', + nodeCount: 0, + status: 'active' as const, + }; + }, [currentChatData]); + + const existingChatData = useMemo(() => { + if (!currentChatData) return null; + return { + interactionId: currentChatData.interactionId, + workflowId: currentChatData.workflowId, + workflowName: currentChatData.workflowName, + }; + }, [currentChatData]); + if (loading) { return (
@@ -42,7 +63,7 @@ const CurrentChatInterface: React.FC = ({ onBack }) = ); } - if (!currentChatData) { + if (!currentChatData || !workflow || !existingChatData) { return (
@@ -58,29 +79,14 @@ const CurrentChatInterface: React.FC = ({ onBack }) = ); } - // 현재 채팅 데이터로 워크플로우 객체 생성 - const workflow = { - id: currentChatData.workflowId, - name: currentChatData.workflowName, - filename: currentChatData.workflowName, - author: 'Unknown', - nodeCount: 0, - status: 'active' as const, - }; - - const existingChatData = { - interactionId: currentChatData.interactionId, - workflowId: currentChatData.workflowId, - workflowName: currentChatData.workflowName, - }; - return (
{})} + hideBackButton={true} + onBack={onBack || (() => { })} />
diff --git a/src/app/chat/components/DefaultChatInterface.tsx b/src/app/chat/components/DefaultChatInterface.tsx new file mode 100644 index 00000000..5ae26110 --- /dev/null +++ b/src/app/chat/components/DefaultChatInterface.tsx @@ -0,0 +1,346 @@ +'use client'; +import React, { useState, useRef, useEffect } from 'react'; +import { + FiSend, + FiMessageSquare, + FiClock, + FiArrowLeft, +} from 'react-icons/fi'; +import styles from '@/app/chat/assets/ChatInterface.module.scss'; +import { createNewChat, executeChatMessage } from '@/app/api/chatAPI'; +import { generateInteractionId } from '@/app/api/interactionAPI'; +import toast from 'react-hot-toast'; + +interface IOLog { + log_id: number | string; + workflow_name: string; + workflow_id: string; + input_data: string; + output_data: string; + updated_at: string; +} + +interface ChatNewResponse { + status: string; + message: string; + interaction_id: string; + workflow_id: string; + workflow_name: string; + execution_meta: { + interaction_id: string; + interaction_count: number; + workflow_id: string; + workflow_name: string; + }; + chat_response?: string; + timestamp: string; +} + +interface ChatExecutionResponse { + status: string; + message: string; + user_input: string; + ai_response: string; + interaction_id: string; + session_id: string; + execution_meta: { + interaction_id: string; + interaction_count: number; + workflow_id: string; + workflow_name: string; + }; + timestamp: string; +} + +interface DefaultChatInterfaceProps { + onBack?: () => void; +} + +const DefaultChatInterface: React.FC = ({ onBack }) => { + const [ioLogs, setIOLogs] = useState([]); + const [executing, setExecuting] = useState(false); + const [error, setError] = useState(null); + const [inputMessage, setInputMessage] = useState(''); + const [pendingLogId, setPendingLogId] = useState(null); + const [isFirstMessage, setIsFirstMessage] = useState(true); + const [interactionId] = useState(() => generateInteractionId('default_chat')); + + const messagesRef = useRef(null); + + // Default workflow 설정 (default_mode) - 일반 채팅용 + const defaultWorkflow = { + id: 'default_mode', + name: 'default_mode', + filename: 'default_chat', + author: 'System', + nodeCount: 1, + status: 'active' as const, + }; + + const scrollToBottom = () => { + if (messagesRef.current) { + messagesRef.current.scrollTop = messagesRef.current.scrollHeight; + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('ko-KR', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const executeWorkflow = async () => { + if (!inputMessage.trim()) { + return; + } + + setError(null); + setExecuting(true); + const tempId = `pending-${Date.now()}`; + setPendingLogId(tempId); + + // 임시 메시지 추가 + setIOLogs((prev) => [ + ...prev, + { + log_id: tempId, + workflow_name: '일반 채팅', + workflow_id: defaultWorkflow.id, + input_data: inputMessage, + output_data: '', + updated_at: new Date().toISOString(), + }, + ]); + + const currentMessage = inputMessage; + setInputMessage(''); + + // 스크롤을 하단으로 이동 + setTimeout(scrollToBottom, 100); + + try { + let result: ChatNewResponse | ChatExecutionResponse; + + if (isFirstMessage) { + // workflow 검증 + if (defaultWorkflow.id !== 'default_mode' || defaultWorkflow.name !== 'default_mode') { + throw new Error('일반 채팅은 default_mode workflow만 사용 가능합니다.'); + } + + // 새로운 채팅 세션 생성 + result = await createNewChat({ + interaction_id: interactionId, + input_data: currentMessage + }) as ChatNewResponse; + + // 첫 번째 메시지 전송 시 현재 채팅 데이터를 localStorage에 저장 + const currentChatData = { + interactionId: result.interaction_id, + workflowId: result.workflow_id, + workflowName: result.workflow_name, + startedAt: result.timestamp || new Date().toISOString(), + }; + localStorage.setItem('currentChatData', JSON.stringify(currentChatData)); + setIsFirstMessage(false); + } else { + // workflow 검증 + if (defaultWorkflow.id !== 'default_mode' || defaultWorkflow.name !== 'default_mode') { + throw new Error('일반 채팅은 default_mode workflow만 사용 가능합니다.'); + } + + // 기존 채팅 세션 계속 + result = await executeChatMessage({ + user_input: currentMessage, + interaction_id: interactionId, + workflow_id: defaultWorkflow.id, + workflow_name: defaultWorkflow.name + }) as ChatExecutionResponse; + } + + if (result.status === 'success') { + // 결과로 임시 메시지 업데이트 + setIOLogs((prev) => + prev.map((log) => + String(log.log_id) === tempId + ? { + ...log, + output_data: isFirstMessage + ? (result as ChatNewResponse).chat_response || '채팅 세션이 시작되었습니다.' + : (result as ChatExecutionResponse).ai_response || '', + updated_at: result.timestamp || new Date().toISOString(), + } + : log, + ), + ); + setPendingLogId(null); + setTimeout(scrollToBottom, 100); + toast.success(isFirstMessage ? '일반 채팅이 시작되었습니다!' : '메시지가 성공적으로 전송되었습니다!'); + } else { + throw new Error(result.message || 'Unknown error occurred'); + } + } catch (err) { + console.error('Default chat execution failed:', err); + + // 에러로 임시 메시지 업데이트 + setIOLogs((prev) => + prev.map((log) => + String(log.log_id) === tempId + ? { + ...log, + output_data: err instanceof Error ? err.message : '처리 중 오류가 발생했습니다.', + updated_at: new Date().toISOString(), + } + : log, + ), + ); + setPendingLogId(null); + toast.error('메시지 처리 중 오류가 발생했습니다.'); + setTimeout(scrollToBottom, 100); + } finally { + setExecuting(false); + } + }; + + const handleSendMessage = async () => { + const trimmedMessage = inputMessage.trim(); + if (!trimmedMessage || executing) return; + + await executeWorkflow(); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey && !executing) { + e.preventDefault(); + executeWorkflow(); + } + }; + + return ( +
+ {/* Header */} +
+
+ {onBack && ( + + )} +
+

일반 채팅

+

AI와 자유롭게 대화해보세요

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

첫 대화를 시작해보세요!

+

일반 채팅 모드가 준비되었습니다.

+
+
+ + + +
+
+
+ ) : ( + ioLogs.map((log) => ( +
+ {/* User Message */} +
+
+ {log.input_data} +
+
+ {formatDate(log.updated_at)} +
+
+ + {/* AI Response */} +
+
+ {String(log.log_id) === pendingLogId && executing && !log.output_data ? ( +
+ + + +
+ ) : ( + log.output_data + )} +
+
+
+ )) + )} +
+ + {/* Input Area */} +
+
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={executing} + className={styles.messageInput} + /> + +
+ {executing && ( +

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

+ )} + {error && ( +

{error}

+ )} +
+
+
+ ); +}; + +export default DefaultChatInterface; diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index b122f293..5af90113 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -1,83 +1,8 @@ 'use client'; -import React, { useState, useEffect, Suspense } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; -import Sidebar from '@/app/main/components/Sidebar'; -import ChatContent from '@/app/chat/components/ChatContent'; -import ChatHistory from '@/app/chat/components/ChatHistory'; -import CurrentChatInterface from '@/app/chat/components/CurrentChatInterface'; -import { getSidebarItems, createItemClickHandler } from '@/app/_common/components/sidebarConfig'; +import React, { Suspense } from 'react'; +import ChatPageContent from '@/app/chat/components/ChatPageContent'; import styles from '@/app/main/assets/MainPage.module.scss'; -const ChatPageContent: React.FC = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const [activeMode, setActiveMode] = useState<'new-chat' | 'chat-history' | 'current-chat'>('chat-history'); - const sidebarItems = getSidebarItems(); - - // URL 파라미터 확인해서 기존 채팅 모드인지 확인 - useEffect(() => { - const mode = searchParams.get('mode'); - const interactionId = searchParams.get('interaction_id'); - - if (mode === 'current') { - // 현재 채팅 모드로 이동 - setActiveMode('current-chat'); - } else if (mode === 'existing' && interactionId) { - // 기존 채팅 계속하기 모드 - ChatContent로 이동 - setActiveMode('new-chat'); - } else { - // 기본적으로 채팅 히스토리 표시 - setActiveMode('chat-history'); - } - }, [searchParams]); - - // 채팅 모드 변경 핸들러 - const handleChatModeClick = (mode: string) => { - if (mode === 'new-chat' || mode === 'chat-history' || mode === 'current-chat') { - setActiveMode(mode); - } else { - // 다른 메뉴 아이템들은 기존 핸들러 사용 - const handleItemClick = createItemClickHandler(router); - handleItemClick(mode); - } - }; - - // 채팅 선택 처리 - const handleChatSelect = (executionMeta: any) => { - // 선택된 채팅을 현재 채팅으로 설정 후 current-chat 모드로 전환 - console.log('Selected chat:', executionMeta); - setActiveMode('current-chat'); - }; - - const renderContent = () => { - switch (activeMode) { - case 'new-chat': - return ; - case 'chat-history': - return ; - case 'current-chat': - return ; - default: - return ; - } - }; - - return ( -
- -
- {renderContent()} -
-
- ); -}; - const ChatPage: React.FC = () => { return ( { const router = useRouter(); const pathname = usePathname(); const [activeSection, setActiveSection] = useState('canvas'); - // Executor/Monitoring 통합 토글 상태 const [execTab, setExecTab] = useState<'executor' | 'monitoring'>( 'executor', ); const [initialLoad, setInitialLoad] = useState(true); - // URL 파라미터 기반 초기 라우팅 처리 useEffect(() => { - // Check URL parameters first for direct navigation const view = searchParams.get('view'); const workflowName = searchParams.get('workflowName'); const workflowId = searchParams.get('workflowId'); if (view === 'playground') { setActiveSection('exec-monitor'); - // 워크플로우 ID가 있으면 executor 탭으로 설정 if (workflowName && workflowId) { setExecTab('executor'); } else { - // 워크플로우 ID가 없으면 저장된 탭을 사용 const savedTab = localStorage.getItem('execMonitorTab'); if (savedTab === 'executor' || savedTab === 'monitoring') { setExecTab(savedTab as 'executor' | 'monitoring'); @@ -46,14 +41,12 @@ const MainPageContent: React.FC = () => { } setInitialLoad(false); } else { - // Check if there's a saved activeSection from chat page navigation const savedActiveSection = localStorage.getItem('activeSection'); if (savedActiveSection && ['canvas', 'workflows', 'exec-monitor', 'settings', 'config-viewer'].includes(savedActiveSection)) { setActiveSection(savedActiveSection); - localStorage.removeItem('activeSection'); // Clear after use + localStorage.removeItem('activeSection'); } - // If no view parameter, load from localStorage const savedTab = localStorage.getItem('execMonitorTab'); if (savedTab === 'executor' || savedTab === 'monitoring') { setExecTab(savedTab as 'executor' | 'monitoring'); @@ -62,10 +55,8 @@ const MainPageContent: React.FC = () => { } }, [searchParams]); - // activeSection 변경 시 URL 초기화 처리 useEffect(() => { if (!initialLoad && activeSection !== 'exec-monitor') { - // URL에서 파라미터를 제거하고 기본 /main 경로로 변경 const currentParams = new URLSearchParams(searchParams.toString()); if ( currentParams.has('view') || @@ -77,25 +68,31 @@ const MainPageContent: React.FC = () => { } }, [activeSection, initialLoad, pathname, router, searchParams]); - // 탭 변경 시 로컬 스토리지에 상태 저장 const handleTabChange = (tab: 'executor' | 'monitoring') => { setExecTab(tab); localStorage.setItem('execMonitorTab', tab); }; - // 사이드바 아이템 클릭 처리 const handleSidebarItemClick = (id: string) => { - setActiveSection(id); + // 채팅 관련 아이템인지 확인 + const chatItems = ['new-chat', 'current-chat', 'chat-history']; + if (chatItems.includes(id)) { + // 채팅 아이템인 경우 localStorage에 저장하고 /chat으로 이동 + const chatItemClickHandler = createChatItemClickHandler(router); + chatItemClickHandler(id); + } else { + // 설정 관련 아이템인 경우 현재 페이지에서 섹션 변경 + setActiveSection(id); + } }; - // 채팅 선택 처리 (Main 화면에서 기존 채팅 세션 표시) const handleChatSelect = (executionMeta: any) => { - // 선택된 채팅을 현재 채팅으로 설정 후 current-chat 모드로 전환 console.log('Selected chat:', executionMeta); setActiveSection('current-chat'); }; - const sidebarItems = getSidebarItems(); + const settingSidebarItems = getSettingSidebarItems(); + const chatSidebarItems = getChatSidebarItems(); // 헤더에 표시할 토글 버튼 const renderExecMonitorToggleButtons = () => ( @@ -215,7 +212,8 @@ const MainPageContent: React.FC = () => { return (
void; className?: string;