diff --git a/src/app/(common)/components/ToastProvider.jsx b/src/app/_common/components/ToastProvider.jsx similarity index 100% rename from src/app/(common)/components/ToastProvider.jsx rename to src/app/_common/components/ToastProvider.jsx diff --git a/src/app/(common)/components/nodeHook.ts b/src/app/_common/components/nodeHook.ts similarity index 100% rename from src/app/(common)/components/nodeHook.ts rename to src/app/_common/components/nodeHook.ts diff --git a/src/app/_common/components/sidebarConfig.ts b/src/app/_common/components/sidebarConfig.ts new file mode 100644 index 00000000..64e85bfd --- /dev/null +++ b/src/app/_common/components/sidebarConfig.ts @@ -0,0 +1,52 @@ +import React from 'react'; +import { + FiGrid, + FiFolder, + FiCpu, + FiSettings, + FiEye, +} from 'react-icons/fi'; +import { SidebarItem } from '@/app/main/components/types'; + +// 워크플로우 관리 센터의 공통 사이드바 아이템들을 반환하는 함수 +export const getSidebarItems = (): SidebarItem[] => [ + { + id: 'canvas', + title: '워크플로우 캔버스', + description: '새로운 워크플로우 만들기', + icon: React.createElement(FiGrid), + }, + { + id: 'workflows', + title: '완성된 워크플로우', + description: '저장된 워크플로우 관리', + icon: React.createElement(FiFolder), + }, + { + id: 'exec-monitor', + title: '실행 및 모니터링', + description: '워크플로우 실행과 성능 모니터링', + icon: React.createElement(FiCpu), + }, + { + id: 'settings', + title: '고급 환경 설정', + description: 'LLM 및 Tool 환경변수 직접 관리', + icon: React.createElement(FiSettings), + }, + { + id: 'config-viewer', + title: '설정값 확인', + description: '백엔드 환경변수 및 설정 확인', + icon: React.createElement(FiEye), + }, +]; + +// 공통 아이템 클릭 핸들러 (localStorage 사용) +export const createItemClickHandler = (router: any) => { + return (itemId: string) => { + // 클릭한 섹션을 localStorage에 저장하고 /main으로 이동 + localStorage.setItem('activeSection', itemId); + router.push('/main'); + }; +}; diff --git a/src/app/(common)/components/workflowStorage.js b/src/app/_common/components/workflowStorage.js similarity index 100% rename from src/app/(common)/components/workflowStorage.js rename to src/app/_common/components/workflowStorage.js diff --git a/src/app/canvas/components/Header.tsx b/src/app/canvas/components/Header.tsx index 0b4b3df0..ccf624c0 100644 --- a/src/app/canvas/components/Header.tsx +++ b/src/app/canvas/components/Header.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from ' import Link from 'next/link'; import styles from '@/app/canvas/assets/Header.module.scss'; import { LuPanelRightOpen, LuSave, LuCheck, LuX, LuPencil, LuFileText } from "react-icons/lu"; -import { getWorkflowName, saveWorkflowName } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowName, saveWorkflowName } from '@/app/_common/components/workflowStorage'; // Type definitions interface HeaderProps { diff --git a/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx b/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx index 9acb64bf..ba4763cc 100644 --- a/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/AddNodePanel.tsx @@ -5,7 +5,7 @@ import NodeList from '@/app/canvas/components/Helper/NodeList'; import DraggableNodeItem from '@/app/canvas/components/Helper/DraggableNodeItem'; import { LuSearch, LuArrowLeft, LuBrainCircuit, LuShare2, LuWrench, LuX, LuRefreshCw } from 'react-icons/lu'; import { SiLangchain } from "react-icons/si"; -import { useNodes } from '@/app/(common)/components/nodeHook'; +import { useNodes } from '@/app/_common/components/nodeHook'; import type { Port, Parameter, diff --git a/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx b/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx index 50a11346..6846b5ef 100644 --- a/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/TemplatePanel.tsx @@ -5,7 +5,7 @@ import styles from '@/app/canvas/assets/WorkflowPanel.module.scss'; import sideMenuStyles from '@/app/canvas/assets/SideMenu.module.scss'; import { LuArrowLeft, LuLayoutTemplate, LuPlay, LuCopy } from "react-icons/lu"; import TemplatePreview from '@/app/canvas/components/SideMenuPanel/TemplatePreview'; -import { getWorkflowState } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowState } from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import Basic_Chatbot from '@/app/canvas/constants/workflow/Basic_Chatbot.json'; diff --git a/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx b/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx index a5263915..0c7214e1 100644 --- a/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx +++ b/src/app/canvas/components/SideMenuPanel/WorkflowPanel.tsx @@ -5,7 +5,7 @@ import styles from '@/app/canvas/assets/WorkflowPanel.module.scss'; import sideMenuStyles from '@/app/canvas/assets/SideMenu.module.scss'; import { LuArrowLeft, LuFolderOpen, LuDownload, LuRefreshCw, LuCalendar, LuTrash2 } from "react-icons/lu"; import { listWorkflows, loadWorkflow, deleteWorkflow } from '@/app/api/workflowAPI'; -import { getWorkflowState } from '@/app/(common)/components/workflowStorage'; +import { getWorkflowState } from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import type { Position, diff --git a/src/app/canvas/page.tsx b/src/app/canvas/page.tsx index 153d7942..eb714306 100644 --- a/src/app/canvas/page.tsx +++ b/src/app/canvas/page.tsx @@ -18,7 +18,7 @@ import { ensureValidWorkflowState, saveWorkflowName, startNewWorkflow, -} from '@/app/(common)/components/workflowStorage'; +} from '@/app/_common/components/workflowStorage'; import { devLog } from '@/app/utils/logger'; import { generateWorkflowHash } from '@/app/utils/generateSha1Hash'; diff --git a/src/app/chat/assets/ChatContent.module.scss b/src/app/chat/assets/ChatContent.module.scss new file mode 100644 index 00000000..de94e921 --- /dev/null +++ b/src/app/chat/assets/ChatContent.module.scss @@ -0,0 +1,166 @@ +// Chat Content Styles +$primary-blue: #2563eb; +$primary-purple: #7c3aed; +$primary-green: #059669; +$gray-50: #f9fafb; +$gray-100: #f3f4f6; +$gray-200: #e5e7eb; +$gray-300: #d1d5db; +$gray-400: #9ca3af; +$gray-500: #6b7280; +$gray-600: #4b5563; +$gray-700: #374151; +$gray-800: #1f2937; +$gray-900: #111827; +$white: #ffffff; + +.chatContainer { + height: 100%; + display: flex; + flex-direction: column; + background: $white; + border-radius: 0.75rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.welcomeSection { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 3rem; + transition: all 0.5s ease; + overflow: hidden; +} + +.workflowSection { + flex: 1; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 2rem; + transition: all 0.5s ease; + overflow: hidden; + + .container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + } +} + +.welcomeContent { + text-align: center; + max-width: 600px; + + h1 { + font-size: 2.5rem; + font-weight: 700; + color: $gray-900; + margin: 0 0 1rem 0; + background: linear-gradient(135deg, $primary-blue 0%, $primary-purple 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + p { + font-size: 1.125rem; + color: $gray-600; + margin: 0 0 3rem 0; + line-height: 1.6; + } +} + +.buttonContainer { + display: flex; + gap: 1.5rem; + margin-top: 2rem; + justify-content: center; + flex-wrap: wrap; +} + +.workflowButton, +.chatButton { + background: $white; + border: 2px solid $gray-200; + border-radius: 1rem; + padding: 2rem 1.5rem; + text-align: center; + transition: all 0.3s ease; + cursor: pointer; + width: 220px; + height: 180px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; + flex-shrink: 0; + + svg { + font-size: 2rem; + color: $primary-blue; + transition: all 0.3s ease; + } + + h3 { + font-size: 1.125rem; + font-weight: 600; + color: $gray-900; + margin: 0; + } + + p { + font-size: 0.875rem; + color: $gray-500; + margin: 0; + line-height: 1.4; + } + + &:hover { + transform: translateY(-3px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + border-color: $primary-blue; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05) 0%, rgba(124, 58, 237, 0.05) 100%); + + svg { + color: $primary-purple; + transform: scale(1.1); + } + } + + &:active { + transform: translateY(-1px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); + } +} + +// Responsive Design +@media (max-width: 768px) { + .welcomeContent { + padding: 1.5rem; + + h1 { + font-size: 2rem; + } + + p { + font-size: 1rem; + } + } + + .buttonContainer { + flex-direction: column; + gap: 1rem; + align-items: center; + } + + .workflowButton, + .chatButton { + width: 100%; + max-width: 280px; + height: 160px; + padding: 1.5rem; + } +} diff --git a/src/app/chat/assets/WorkflowSelection.module.scss b/src/app/chat/assets/WorkflowSelection.module.scss new file mode 100644 index 00000000..3e63f442 --- /dev/null +++ b/src/app/chat/assets/WorkflowSelection.module.scss @@ -0,0 +1,441 @@ +@use "sass:color"; + +// Color Variables +$primary-blue: #2563eb; +$primary-green: #059669; +$primary-yellow: #d97706; +$primary-red: #dc2626; +$gray-50: #f9fafb; +$gray-100: #f3f4f6; +$gray-200: #e5e7eb; +$gray-300: #d1d5db; +$gray-400: #9ca3af; +$gray-500: #6b7280; +$gray-600: #4b5563; +$gray-700: #374151; +$gray-900: #111827; +$white: #ffffff; + +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +// Header +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + flex-shrink: 0; +} + +.headerInfo { + display: flex; + align-items: center; + gap: 1rem; + + h2 { + font-size: 1.875rem; + font-weight: 700; + color: $gray-900; + margin: 0 0 0.5rem 0; + } + + p { + color: $gray-600; + margin: 0; + line-height: 1.6; + } +} + +.backButton { + background: $white; + border: 2px solid $gray-200; + border-radius: 0.5rem; + padding: 0.75rem; + cursor: pointer; + 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; + + svg { + color: $primary-blue; + } + } +} + +.headerActions { + display: flex; + align-items: center; + gap: 1rem; +} + +// Filters +.filters { + display: flex; + gap: 0.5rem; + background: $gray-100; + padding: 0.25rem; + border-radius: 0.5rem; +} + +// 새로고침 버튼 스타일 +.refreshButton { + background: transparent; + border: 1px solid $gray-300; + border-radius: 6px; + padding: 0.5rem; + cursor: pointer; + color: $gray-600; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + + &:hover:not(:disabled) { + background: $gray-50; + border-color: $primary-blue; + color: $primary-blue; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + svg { + width: 16px; + height: 16px; + } +} + +// 스피닝 애니메이션 +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.filterButton { + padding: 0.5rem 1rem; + border: none; + background: transparent; + color: $gray-700; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease-in-out; + font-size: 0.875rem; + + &:hover { + background: $white; + color: $gray-900; + } + + &.active { + background: $white; + color: $primary-blue; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } +} + +// Workflows Grid +.workflowsGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + padding-top: 1rem; + gap: 1rem; + overflow-y: auto; + max-height: 380px; + padding-right: 0.5rem; + + /* 스크롤바 스타일링 */ + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: $gray-100; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: $gray-300; + border-radius: 3px; + + &:hover { + background: $gray-400; + } + } +} + +.workflowCard { + background: $white; + border: 2px solid $gray-200; + border-radius: 0.75rem; + padding: 1rem; + transition: all 0.2s ease-in-out; + cursor: pointer; + position: relative; + min-height: 160px; + max-height: 160px; + display: flex; + flex-direction: column; + + &:hover:not(.disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border-color: $primary-blue; + } + + &:active:not(.disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + + &:hover { + transform: none; + box-shadow: none; + border-color: $gray-200; + } + } +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.workflowIcon { + width: 2rem; + height: 2rem; + background: rgba($primary-blue, 0.1); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 1rem; + height: 1rem; + color: $primary-blue; + } +} + +.status { + padding: 0.2rem 0.5rem; + border-radius: 0.75rem; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + + &.statusActive { + background: rgba($primary-green, 0.1); + color: $primary-green; + } + + &.statusDraft { + background: rgba($primary-yellow, 0.1); + color: $primary-yellow; + } + + &.statusArchived { + background: rgba($gray-500, 0.1); + color: $gray-500; + } +} + +// Card Content +.cardContent { + margin-bottom: 0.75rem; + flex: 1; + display: flex; + flex-direction: column; +} + +.workflowName { + font-size: 0.95rem; + font-weight: 600; + color: $gray-900; + margin: 0 0 0.5rem 0; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workflowDescription { + color: $gray-600; + line-height: 1.4; + margin: 0 0 0.75rem 0; + font-size: 0.8rem; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.workflowError { + color: $primary-red; + line-height: 1.4; + margin: 0 0 0.75rem 0; + font-size: 0.75rem; + padding: 0.4rem; + background: rgba($primary-red, 0.1); + border-radius: 0.375rem; + border-left: 2px solid $primary-red; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.workflowMeta { + display: flex; + flex-direction: column; + gap: 0.3rem; + margin-top: auto; +} + +.metaItem { + display: flex; + align-items: center; + gap: 0.3rem; + color: $gray-500; + font-size: 0.75rem; + + svg { + width: 0.75rem; + height: 0.75rem; + } +} + +// Loading State +.loadingState { + text-align: center; + padding: 4rem 2rem; + color: $gray-500; + + p { + font-size: 1.125rem; + margin: 0; + } +} + +// Error State +.errorState { + text-align: center; + padding: 4rem 2rem; + color: $primary-red; + + p { + font-size: 1.125rem; + margin: 0 0 1rem 0; + } + + button { + padding: 0.5rem 1rem; + background: $primary-blue; + color: $white; + border: none; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s ease-in-out; + + &:hover { + background: color.scale($primary-blue, $lightness: -10%); + } + } +} + +// Empty State +.emptyState { + text-align: center; + padding: 4rem 2rem; + color: $gray-500; + + .emptyIcon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + opacity: 0.5; + } + + h3 { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 0.5rem 0; + color: $gray-700; + } + + p { + margin: 0; + line-height: 1.6; + } +} + +// Responsive Design +@media (max-width: 1024px) { + .workflowsGrid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (max-width: 768px) { + .header { + flex-direction: column; + align-items: stretch; + } + + .filters { + justify-content: center; + } + + .workflowsGrid { + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + } + + .workflowCard { + min-height: 140px; + max-height: 140px; + padding: 0.75rem; + } +} + +@media (max-width: 480px) { + .workflowsGrid { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx new file mode 100644 index 00000000..1a9f4602 --- /dev/null +++ b/src/app/chat/components/ChatContent.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import styles from '@/app/chat/assets/ChatContent.module.scss'; +import { LuWorkflow } from "react-icons/lu"; +import { IoChatbubblesOutline } from "react-icons/io5"; +import WorkflowSelection from './WorkflowSelection'; + +const ChatContent: React.FC = () => { + const [currentView, setCurrentView] = useState<'welcome' | 'workflow'>('welcome'); + + const handleWorkflowSelect = (workflow: any) => { + // 워크플로우 선택 후 로직 (나중에 구현) + console.log('Selected workflow:', workflow); + }; + + if (currentView === 'workflow') { + return ( +
+
+ setCurrentView('welcome')} + onSelectWorkflow={handleWorkflowSelect} + /> +
+
+ ); + } + + return ( +
+
+
+

채팅을 시작하세요! 🚀

+

AI와 대화하며 궁금한 물어보세요.

+
+ + +
+
+
+
+ ); +}; + +export default ChatContent; diff --git a/src/app/chat/components/WorkflowSelection.tsx b/src/app/chat/components/WorkflowSelection.tsx new file mode 100644 index 00000000..2c887b01 --- /dev/null +++ b/src/app/chat/components/WorkflowSelection.tsx @@ -0,0 +1,254 @@ +'use client'; +import React, { useState, useEffect } from 'react'; +import { + FiFolder, + FiPlay, + FiUser, + FiClock, + FiRefreshCw, + FiArrowLeft, +} from 'react-icons/fi'; +import styles from '@/app/chat/assets/WorkflowSelection.module.scss'; +import { listWorkflowsDetail } from '@/app/api/workflowAPI'; +import toast from 'react-hot-toast'; + +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 WorkflowDetailResponse { + filename: string; + workflow_id: string; + node_count: number; + last_modified: string; + has_startnode: boolean; + has_endnode: boolean; + error?: string; +} + +interface WorkflowSelectionProps { + onBack: () => void; + onSelectWorkflow: (workflow: Workflow) => void; +} + +const WorkflowSelection: React.FC = ({ onBack, onSelectWorkflow }) => { + const [workflows, setWorkflows] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filter, setFilter] = useState<'all' | 'active' | 'draft'>('active'); + + const fetchWorkflows = async () => { + try { + setLoading(true); + setError(null); + const workflowDetails = (await listWorkflowsDetail()) as WorkflowDetailResponse[]; + const transformedWorkflows: Workflow[] = workflowDetails.map( + (detail: WorkflowDetailResponse) => { + let status: 'active' | 'draft' | 'archived' = 'active'; + if ( + !detail.has_startnode || + !detail.has_endnode || + detail.node_count < 3 + ) { + status = 'draft'; + } + + return { + id: detail.workflow_id, + name: detail.filename.replace('.json', '') || detail.workflow_id, + author: 'AI-LAB', + nodeCount: detail.node_count, + lastModified: detail.last_modified, + status: status, + filename: detail.filename, + error: detail.error, + }; + }, + ); + + setWorkflows(transformedWorkflows); + } catch (error) { + console.error('Failed to fetch workflows:', error); + setError('워크플로우를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchWorkflows(); + }, []); + + const filteredWorkflows = workflows.filter( + (workflow) => filter === 'all' || workflow.status === filter, + ); + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': + return styles.statusActive; + case 'draft': + return styles.statusDraft; + case 'archived': + return styles.statusArchived; + default: + return styles.statusActive; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': + return '활성'; + case 'draft': + return '초안'; + case 'archived': + return '보관됨'; + default: + return '활성'; + } + }; + + const handleSelectWorkflow = (workflow: Workflow) => { + if (workflow.status === 'active') { + onSelectWorkflow(workflow); + toast.success(`"${workflow.name}" 워크플로우를 선택했습니다!`); + } else { + toast.error('활성 상태의 워크플로우만 선택할 수 있습니다.'); + } + }; + + return ( +
+ {/* Header */} +
+
+ +
+

워크플로우 선택

+

채팅에 사용할 워크플로우를 선택하세요.

+
+
+ +
+
+ {['all', 'active', 'draft'].map((filterType) => ( + + ))} +
+ + +
+
+ + {/* Loading State */} + {loading && ( +
+

워크플로우를 불러오는 중...

+
+ )} + + {/* Error State */} + {error && ( +
+

{error}

+ +
+ )} + + {/* Workflows Grid */} + {!loading && !error && ( +
+ {filteredWorkflows.map((workflow) => ( +
handleSelectWorkflow(workflow)} + > +
+
+ +
+
+ {getStatusText(workflow.status)} +
+
+ +
+

+ {workflow.name} +

+ {workflow.description && ( +

+ {workflow.description} +

+ )} + {workflow.error && ( +

+ 오류: {workflow.error} +

+ )} + +
+
+ + {workflow.author} +
+ {workflow.lastModified && ( +
+ + + {new Date(workflow.lastModified).toLocaleDateString('ko-KR')} + +
+ )} +
+ {workflow.nodeCount}개 노드 +
+
+
+
+ ))} +
+ )} + + {!loading && !error && filteredWorkflows.length === 0 && ( +
+ +

워크플로우가 없습니다

+

아직 저장된 워크플로우가 없습니다. 새로운 워크플로우를 만들어보세요.

+
+ )} +
+ ); +}; + +export default WorkflowSelection; diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx new file mode 100644 index 00000000..cdcab9ed --- /dev/null +++ b/src/app/chat/page.tsx @@ -0,0 +1,30 @@ +'use client'; +import React from 'react'; +import { useRouter } from 'next/navigation'; +import Sidebar from '@/app/main/components/Sidebar'; +import ChatContent from '@/app/chat/components/ChatContent'; +import { getSidebarItems, createItemClickHandler } from '@/app/_common/components/sidebarConfig'; +import styles from '@/app/main/assets/MainPage.module.scss'; + +const ChatPage: React.FC = () => { + const router = useRouter(); + const sidebarItems = getSidebarItems(); + const handleItemClick = createItemClickHandler(router); + + return ( +
+ +
+ +
+
+ ); +}; + +export default ChatPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 36594553..abccefb3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next'; import '@/app/globals.css'; -import ToastProvider from '@/app/(common)/components/ToastProvider'; +import ToastProvider from '@/app/_common/components/ToastProvider'; export const metadata: Metadata = { title: 'PlateeRAG', diff --git a/src/app/main/components/MainPageContent.tsx b/src/app/main/components/MainPageContent.tsx index 54bf496d..7be3db3b 100644 --- a/src/app/main/components/MainPageContent.tsx +++ b/src/app/main/components/MainPageContent.tsx @@ -1,13 +1,5 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { - FiGrid, - FiFolder, - FiCpu, - FiSettings, - FiEye, - FiBarChart, -} from 'react-icons/fi'; import Sidebar from '@/app/main/components/Sidebar'; import ContentArea from '@/app/main/components/ContentArea'; import CanvasIntroduction from '@/app/main/components/CanvasIntroduction'; @@ -15,7 +7,7 @@ import CompletedWorkflows from '@/app/main/components/CompletedWorkflows'; import Playground from '@/app/main/components/Playground'; import Settings from '@/app/main/components/Settings'; import ConfigViewer from '@/app/main/components/ConfigViewer'; -import { SidebarItem } from '@/app/main/components/types'; +import { getSidebarItems } from '@/app/_common/components/sidebarConfig'; import styles from '@/app/main/assets/MainPage.module.scss'; import { useSearchParams, useRouter, usePathname } from 'next/navigation'; @@ -51,6 +43,13 @@ 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 + } + // If no view parameter, load from localStorage const savedTab = localStorage.getItem('execMonitorTab'); if (savedTab === 'executor' || savedTab === 'monitoring') { @@ -86,38 +85,7 @@ const MainPageContent: React.FC = () => { setActiveSection(id); }; - const sidebarItems: SidebarItem[] = [ - { - id: 'canvas', - title: '워크플로우 캔버스', - description: '새로운 워크플로우 만들기', - icon: , - }, - { - id: 'workflows', - title: '완성된 워크플로우', - description: '저장된 워크플로우 관리', - icon: , - }, - { - id: 'exec-monitor', - title: '실행 및 모니터링', - description: '워크플로우 실행과 성능 모니터링', - icon: , - }, - { - id: 'settings', - title: '고급 환경 설정', - description: 'LLM 및 Tool 환경변수 직접 관리', - icon: , - }, - { - id: 'config-viewer', - title: '설정값 확인', - description: '백엔드 환경변수 및 설정 확인', - icon: , - }, - ]; + const sidebarItems = getSidebarItems(); // 헤더에 표시할 토글 버튼 const renderExecMonitorToggleButtons = () => ( @@ -215,6 +183,8 @@ const MainPageContent: React.FC = () => { items={sidebarItems} activeItem={activeSection} onItemClick={handleSidebarItemClick} + initialChatExpanded={false} + initialSettingExpanded={true} />
{renderContent()}
diff --git a/src/app/main/components/Sidebar.tsx b/src/app/main/components/Sidebar.tsx index a7d335e7..43fb0cbd 100644 --- a/src/app/main/components/Sidebar.tsx +++ b/src/app/main/components/Sidebar.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; +import { RiChatSmileAiLine } from "react-icons/ri"; import { SidebarProps } from '@/app/main/components/types'; import styles from '@/app/main/assets/MainPage.module.scss'; @@ -9,10 +10,12 @@ const Sidebar: React.FC = ({ activeItem, onItemClick, className = '', + initialChatExpanded = false, + initialSettingExpanded = false, }) => { const router = useRouter(); - const [isSettingExpanded, setIsSettingExpanded] = useState(false); - const [isChatExpanded, setIsChatExpanded] = useState(false); + const [isSettingExpanded, setIsSettingExpanded] = useState(initialSettingExpanded); + const [isChatExpanded, setIsChatExpanded] = useState(initialChatExpanded); const toggleExpanded = () => { setIsSettingExpanded(!isSettingExpanded); @@ -32,6 +35,10 @@ const Sidebar: React.FC = ({ router.push('/'); }; + const handleNewChatClick = () => { + router.push('/chat'); + }; + return (