diff --git a/src/app/chat/assets/ChatInterface.module.scss b/src/app/chat/assets/ChatInterface.module.scss index 2e7fa2ce..ceec378d 100644 --- a/src/app/chat/assets/ChatInterface.module.scss +++ b/src/app/chat/assets/ChatInterface.module.scss @@ -769,6 +769,210 @@ $white: #ffffff; } } +.headerActions { + display: flex; + align-items: center; + gap: 1rem; +} + +.deployButton { + display: flex; + align-items: center; + gap: 0.5rem; + background: $primary-green; + color: $white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; + + &:hover { + background: #047857; + transform: translateY(-1px); + } +} + +// Deployment Modal Styles +.deploymentModalBackdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; +} + +.deploymentModalContainer { + background: $white; + border-radius: 0.75rem; + width: 90%; + max-width: 700px; + max-height: 80vh; + display: flex; + flex-direction: column; +} + +.deploymentModalHeader { + padding: 1rem 1.5rem; + border-bottom: 1px solid $gray-200; + display: flex; + justify-content: space-between; + align-items: center; + + h3 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + } +} + +.closeButton { + background: none; + border: none; + padding: 0.5rem; + margin: -0.5rem; + cursor: pointer; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: $gray-500; + transition: all 0.2s ease; + + &:hover { + background-color: $gray-100; + color: $gray-800; + } + + svg { + width: 20px; + height: 20px; + } +} + +.deploymentModalContent { + padding: 1.5rem; + overflow-y: auto; +} + +.deploymentSection { + margin-bottom: 2rem; + + h4 { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + font-weight: 600; + margin: 0 0 0.75rem 0; + } + + p { + font-size: 0.875rem; + color: $gray-600; + margin: 0 0 1rem 0; + } +} + +.webPageUrl { + display: flex; + align-items: center; + gap: 0.5rem; + background: $gray-100; + padding: 0.75rem; + border-radius: 0.5rem; + + a { + flex: 1; + color: $primary-blue; + text-decoration: none; + word-break: break-all; + &:hover { + text-decoration: underline; + } + } + + button { + background: $primary-blue; + color: $white; + border: none; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + cursor: pointer; + } +} + +.codeSnippet { + background: $gray-800; + color: $gray-100; + padding: 1rem; + border-radius: 0.5rem; + font-family: monospace; + white-space: pre-wrap; + word-break: break-all; + font-size: 0.875rem; +} + +.tabContainer { + display: flex; + border-bottom: 1px solid $gray-200; + padding: 0 1.5rem; + margin-top: 0.5rem; +} + +.tabButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border: none; + background: none; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + color: $gray-500; + border-bottom: 2px solid transparent; + transition: all 0.2s ease-in-out; + margin-bottom: -1px; + + &:hover { + color: $gray-800; + } + + &.active { + color: $primary-blue; + border-bottom-color: $primary-blue; + } +} + +.deploymentModalContent { + padding: 0; + overflow-y: auto; + max-height: calc(80vh - 120px); +} + +.tabPanel { + padding: 1.5rem; + + p { + font-size: 0.875rem; + color: $gray-600; + margin: 0 0 1rem 0; + } + + h5 { + margin-top: 1.5rem; + margin-bottom: 0.5rem; + } +} + // Responsive Design @media (max-width: 768px) { .header { diff --git a/src/app/chat/components/ChatArea.tsx b/src/app/chat/components/ChatArea.tsx index f204f245..25007505 100644 --- a/src/app/chat/components/ChatArea.tsx +++ b/src/app/chat/components/ChatArea.tsx @@ -5,7 +5,7 @@ import { EmptyState } from './EmptyState'; import { IOLog, Workflow } from './types'; interface ChatAreaProps { - mode: "existing" | "new-workflow" | "new-default"; + mode: "existing" | "new-workflow" | "new-default" | "deploy"; loading: boolean; ioLogs: IOLog[]; workflow: Workflow; diff --git a/src/app/chat/components/ChatContent.tsx b/src/app/chat/components/ChatContent.tsx index 0ec84ced..69348063 100644 --- a/src/app/chat/components/ChatContent.tsx +++ b/src/app/chat/components/ChatContent.tsx @@ -24,7 +24,6 @@ const ChatContentInner: React.FC = ({ onChatStarted }) => { const workflowName = searchParams.get('workflow_name'); if (mode === 'existing' && interactionId && workflowId && workflowName) { - // 기존 채팅 정보를 설정하고 바로 채팅 화면으로 이동 const existingWorkflow = { id: workflowId, name: workflowName, diff --git a/src/app/chat/components/ChatHeader.tsx b/src/app/chat/components/ChatHeader.tsx index 7e268536..e6f933c2 100644 --- a/src/app/chat/components/ChatHeader.tsx +++ b/src/app/chat/components/ChatHeader.tsx @@ -1,8 +1,8 @@ -import { FiArrowLeft, FiMessageSquare } from 'react-icons/fi'; +import { FiArrowLeft, FiMessageSquare, FiUpload } 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 }) => { +const ChatHeader: React.FC = ({ mode, workflow, ioLogs, onBack, hideBackButton, onDeploy }) => { let title = ''; let subtitle = ''; let chatCountText = ''; @@ -17,6 +17,10 @@ const ChatHeader: React.FC = ({ mode, workflow, ioLogs, onBack, title = '일반 채팅'; subtitle = '자유롭게 대화를 시작하세요'; chatCountText = '새 채팅'; + } else if (mode === 'new-workflow') { + title = workflow.name; + subtitle = '새로운 대화를 시작하세요'; + chatCountText = '새 채팅'; } else { title = workflow.name; subtitle = '새로운 대화를 시작하세요'; @@ -39,9 +43,19 @@ const ChatHeader: React.FC = ({ mode, workflow, ioLogs, onBack,
+ { mode === 'deploy' ? ( + + ) : ( + + )} {chatCountText} +
+ ); }; diff --git a/src/app/chat/components/ChatInterface.tsx b/src/app/chat/components/ChatInterface.tsx index 6ef0d962..2f7c22c3 100644 --- a/src/app/chat/components/ChatInterface.tsx +++ b/src/app/chat/components/ChatInterface.tsx @@ -20,6 +20,7 @@ import CollectionModal from '@/app/chat/components/CollectionModal'; import { IOLog, ChatInterfaceProps } from './types'; import ChatHeader from './ChatHeader'; import { ChatArea } from './ChatArea'; +import { DeploymentModal } from './DeploymentModal'; const ChatInterface: React.FC = ({ mode, workflow, onBack, hideBackButton = false, existingChatData }) => { @@ -32,6 +33,8 @@ const ChatInterface: React.FC = ({ mode, workflow, onBack, h const [showAttachmentMenu, setShowAttachmentMenu] = useState(false); const [showCollectionModal, setShowCollectionModal] = useState(false); const [selectedCollection, setSelectedCollection] = useState(null); + const [showDeploymentModal, setShowDeploymentModal] = useState(false); + const messagesRef = useRef(null); const attachmentButtonRef = useRef(null); @@ -284,6 +287,7 @@ const ChatInterface: React.FC = ({ mode, workflow, onBack, h ioLogs={ioLogs} onBack={onBack} hideBackButton={hideBackButton} + onDeploy={() => setShowDeploymentModal(true)} > {/* Chat Area */} @@ -401,6 +405,11 @@ const ChatInterface: React.FC = ({ mode, workflow, onBack, h + setShowDeploymentModal(false)} + workflow={workflow} + /> {/* Collection Modal */} void; + workflow: Workflow; +} +export const DeploymentModal: React.FC = ({ isOpen, onClose, workflow }) => { + const [baseUrl, setBaseUrl] = useState(''); + const [activeTab, setActiveTab] = useState('website'); + const closeButtonRef = useRef(null); + + useEffect(() => { + if (typeof window !== 'undefined') { + setBaseUrl(window.location.origin); + } + }, []); + + useEffect(() => { + if (isOpen) { + setActiveTab('website'); + setTimeout(() => closeButtonRef.current?.focus(), 100); + } + }, [isOpen]); + + if (!isOpen) return null; + + const handleBackdropKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + const chatId = workflow.id; + const workflowName = encodeURIComponent(workflow.name); + const apiEndpoint = `${baseUrl}/api/workflow/execute/based_id`; + const webPageUrl = `${baseUrl}/chatbot/${chatId}?workflowName=${workflowName}`; + + const pythonApiCode = `import requests + +API_URL = "${apiEndpoint}" + +def query(payload): + response = requests.post(API_URL, json=payload) + return response.json() + +output = query({ + "workflow_name": ${workflowName}, + "workflow_id": ${chatId}, + "input_data": "안녕하세요", + "interaction_id": "default", + "selected_collection": "string" +}) +`; + + const jsApiCode = `async function query(data) { + const response = await fetch( + "${apiEndpoint}", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + } + ); + const result = await response.json(); + return result; +} + +query({ + "workflow_name": ${workflowName}, + "workflow_id": ${chatId}, + "input_data": "안녕하세요", + "interaction_id": "default", + "selected_collection": "string" +}).then((response) => { + console.log(response); +}); +`; + + + return ( +
+
+
+

배포 정보: {workflow.name}

+ +
+ + {/* 탭 버튼 UI */} +
+ + +
+ + {/* 탭 컨텐츠 */} +
+ {activeTab === 'website' && ( +
+

아래 링크를 통해 독립된 웹페이지에서 채팅을 사용할 수 있습니다.

+
+ + {baseUrl ? webPageUrl : 'URL 생성 중...'} + + +
+
+ )} + + {activeTab === 'api' && ( +
+

아래 코드를 사용하여 API를 통해 워크플로우를 호출할 수 있습니다.

+
Python
+
{baseUrl ? pythonApiCode : '코드 생성 중...'}
+
JavaScript
+
{baseUrl ? jsApiCode : '코드 생성 중...'}
+
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/chat/components/types.tsx b/src/app/chat/components/types.tsx index fc3ce8a0..cd00a559 100644 --- a/src/app/chat/components/types.tsx +++ b/src/app/chat/components/types.tsx @@ -21,7 +21,7 @@ export interface IOLog { } export interface ChatInterfaceProps { - mode: 'existing' | 'new-workflow' | 'new-default'; + mode: 'existing' | 'new-workflow' | 'new-default' | 'deploy'; workflow: Workflow; onChatStarted?: (newInteractionId: string) => void; onBack: () => void; @@ -34,9 +34,10 @@ export interface ChatInterfaceProps { } export interface ChatHeaderProps { - mode: 'existing' | 'new-workflow' | 'new-default'; + mode: 'existing' | 'new-workflow' | 'new-default' | 'deploy'; workflow: Workflow; ioLogs: IOLog[]; onBack: () => void; hideBackButton?: boolean; + onDeploy?: () => void; } \ No newline at end of file diff --git a/src/app/chatbot/[chatId]/StandaloneChat.module.scss b/src/app/chatbot/[chatId]/StandaloneChat.module.scss new file mode 100644 index 00000000..f5da10a2 --- /dev/null +++ b/src/app/chatbot/[chatId]/StandaloneChat.module.scss @@ -0,0 +1,58 @@ +.pageContainer { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; + background-color: #f0f2f5; + + & > div { + width: 100%; + height: 100%; + max-width: 1200px; + border-radius: 0; + box-shadow: none; + } +} + +.centeredMessage { + text-align: center; + color: #4b5563; + + h2 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + } + + .homeButton { + margin-top: 1.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + background-color: #10b981; + color: white; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #059669; + } + } +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #10b981; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/app/chatbot/[chatId]/page.tsx b/src/app/chatbot/[chatId]/page.tsx new file mode 100644 index 00000000..0d822288 --- /dev/null +++ b/src/app/chatbot/[chatId]/page.tsx @@ -0,0 +1,93 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; +import { loadWorkflow } from '@/app/api/workflowAPI'; +import ChatInterface from '@/app/chat/components/ChatInterface'; +import { Workflow } from '@/app/chat/components/types'; +import styles from './StandaloneChat.module.scss'; + +const StandaloneChatPage = () => { + const params = useParams(); + const searchParams = useSearchParams(); + const router = useRouter(); + const chatId = params.chatId as string; + const workflowNameFromUrl = searchParams.get('workflowName') as string; + + + const [workflow, setWorkflow] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!chatId) { + setError('잘못된 접근입니다. 워크플로우 ID가 필요합니다.'); + setLoading(false); + return; + } + + const fetchWorkflow = async () => { + try { + setLoading(true); + const fetchedWorkflow: Workflow | null = { + id: chatId, + name: workflowNameFromUrl, + filename: workflowNameFromUrl, + author: 'Unknown', + nodeCount: 0, + status: 'active' as const, + }; + setWorkflow(fetchedWorkflow); + setError(null); + } catch (err) { + console.error(err); + setError('워크플로우를 불러오는 데 실패했습니다. ID를 확인해 주세요.'); + setWorkflow(null); + } finally { + setLoading(false); + } + }; + + fetchWorkflow(); + }, [chatId]); + + if (loading) { + return ( +
+
+
+

채팅 인터페이스를 불러오는 중입니다...

+
+
+ ); + } + + if (error || !workflow) { + return ( +
+
+

오류

+

{error || '워크플로우를 찾을 수 없습니다.'}

+ +
+
+ ); + } + + return ( +
+ {}} + onChatStarted={() => {}} + hideBackButton={true} + existingChatData={undefined} + /> +
+ ); +}; + +export default StandaloneChatPage; \ No newline at end of file