init
This commit is contained in:
57
frontend/dev_chatbot/AIService.js
Normal file
57
frontend/dev_chatbot/AIService.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// AIService.js
|
||||
// -----------------------------------------------------------------------------
|
||||
// 백엔드 `/chat` 엔드포인트 호출 전용 모듈.
|
||||
// - question : 사용자가 보낸 텍스트
|
||||
// - toolId : 엔진 ID (ex: dev_chatbot)
|
||||
// - sessionId : 세션 지속용 ID (null이면 서버가 새로 발급)
|
||||
// - files : 첨부 파일 배열
|
||||
// 응답 형태 { response, sessionId, toolName }
|
||||
// 프론트엔드의 ChatHandler / ChatInput 등에서 재사용한다.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 실제 운영환경에서는 REACT_APP_* 형태의 .env 값을 사용하도록 권장.
|
||||
const OPENAI_API_KEY = process.env.REACT_APP_OPENAI_API_KEY;
|
||||
|
||||
// FastAPI 서버 주소 – 필요시 프록시 / env 로 분리.
|
||||
const API_BASE_URL = 'http://localhost:8010';
|
||||
|
||||
/**
|
||||
* 챗봇 API 호출 함수 (multipart/form-data)
|
||||
* @param {string} question - 사용자 질문
|
||||
* @param {string} toolId - 엔진 ID
|
||||
* @param {string|null} sessionId- 세션 ID (옵션)
|
||||
* @param {File[]} files - 첨부 파일 배열
|
||||
* @returns {Promise<{response:string, sessionId:string, toolName:string}>}
|
||||
*/
|
||||
const AIService = async (question, toolId, sessionId = null, files = []) => {
|
||||
try {
|
||||
// ------------ FormData 구성 -------------
|
||||
const formData = new FormData();
|
||||
formData.append('message', question);
|
||||
formData.append('tool_id', toolId);
|
||||
|
||||
if (sessionId) formData.append('session_id', sessionId);
|
||||
|
||||
files.forEach(file => formData.append('image', file));
|
||||
|
||||
// ------------- Fetch -------------------
|
||||
const response = await fetch(`${API_BASE_URL}/chat`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
response : data.response?.trim() || 'AI 응답을 생성하지 못했습니다.',
|
||||
sessionId: data.session_id,
|
||||
toolName : data.tool_name,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('AI 서비스 오류:', e);
|
||||
return { response: 'AI 응답을 생성하지 못했습니다.', sessionId: null, toolName: '' };
|
||||
}
|
||||
};
|
||||
|
||||
export default AIService;
|
||||
40
frontend/dev_chatbot/ChatHandler.js
Normal file
40
frontend/dev_chatbot/ChatHandler.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// ChatHandler.js
|
||||
// -----------------------------------------------------------------------------
|
||||
// 1) 이미지가 있을 경우 OCRService 로 텍스트 추출
|
||||
// 2) 최종 question(원문 + OCR 텍스트) 을 AIService 로 전달
|
||||
// 3) AIService 응답을 그대로 반환하여 상위 컴포넌트(ChatInput)에서 처리
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import OCRService from './OCRService';
|
||||
import AIService from './AIService';
|
||||
|
||||
/**
|
||||
* 사용자의 입력(text + image)을 받아 AIService 로 요청하는 헬퍼.
|
||||
* @param {Object} params
|
||||
* @param {string} params.text - 사용자가 입력한 텍스트
|
||||
* @param {File[]} params.files - 첨부 파일 배열(선택)
|
||||
* @param {string} params.toolId - 호출할 엔진 ID
|
||||
* @param {string=} params.sessionId - 세션 ID (선택)
|
||||
* @returns {Promise<{response:string, sessionId:string, toolName:string}>}
|
||||
*/
|
||||
const ChatHandler = async ({ text, files = [], toolId, sessionId = null }) => {
|
||||
let question = text;
|
||||
|
||||
// 이미지 파일이 있으면 OCR 실행 후 question 에 병합
|
||||
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
||||
if (imageFiles.length) {
|
||||
const ocrTexts = [];
|
||||
for (const img of imageFiles) {
|
||||
const ocrText = await OCRService(img);
|
||||
if (ocrText) ocrTexts.push(ocrText);
|
||||
}
|
||||
if (ocrTexts.length) {
|
||||
question = `${text}\n\n이미지에서 추출한 텍스트:\n${ocrTexts.join('\n')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// AI 서비스 호출
|
||||
return AIService(question, toolId, sessionId, files);
|
||||
};
|
||||
|
||||
export default ChatHandler;
|
||||
93
frontend/dev_chatbot/ChatInput.jsx
Normal file
93
frontend/dev_chatbot/ChatInput.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
// ChatInput.jsx (간단한 상태 기반 데모 컴포넌트)
|
||||
// -----------------------------------------------------------------------------
|
||||
// • inputText / inputImage 상태 관리
|
||||
// • ChatHandler 호출 후 messages 배열에 push
|
||||
// • very minimal UI (실제 서비스에서는 스타일·UX 개선 필요)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import ChatHandler from './ChatHandler';
|
||||
|
||||
const ChatInput = () => {
|
||||
// ---------------- state ----------------
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [selectedFiles, setSelectedFiles] = useState([]); // File[]
|
||||
const [messages, setMessages] = useState([]); // {text, files, sender}
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// ------------- handlers ---------------
|
||||
const handleTextChange = e => setInputText(e.target.value);
|
||||
|
||||
const handleFilesChange = e => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length) {
|
||||
setSelectedFiles(prev => [...prev, ...files]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileRemove = idx => {
|
||||
setSelectedFiles(prev => prev.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
// 채팅 보내기
|
||||
const handleSend = async () => {
|
||||
if (!inputText && selectedFiles.length === 0) return;
|
||||
setLoading(true);
|
||||
|
||||
// 1) 사용자 메시지 화면에 표시
|
||||
const userMessage = { text: inputText, files: selectedFiles, sender: 'user' };
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
|
||||
// 2) ChatHandler 로 AI 답변 요청
|
||||
const { response } = await ChatHandler({
|
||||
text : inputText,
|
||||
files : selectedFiles,
|
||||
toolId : 'dev_chatbot', // 데모용 고정
|
||||
});
|
||||
|
||||
// 3) AI 응답 메시지 push
|
||||
setMessages(prev => [...prev, userMessage, { text: response, sender: 'ai' }]);
|
||||
|
||||
// 4) 입력 초기화
|
||||
setInputText('');
|
||||
setSelectedFiles([]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// ---------------- render --------------
|
||||
return (
|
||||
<div>
|
||||
<div style={{ minHeight: 200, border: '1px solid #ccc', marginBottom: 10, padding: 10 }}>
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} style={{ marginBottom: 8 }}>
|
||||
<b>{msg.sender === 'user' ? '나' : 'AI'}:</b> {msg.text}
|
||||
{msg.files && msg.files.length > 0 && (
|
||||
<ul style={{ marginTop: 4 }}>
|
||||
{msg.files.map((file, fIdx) => (
|
||||
<li key={fIdx}>{file.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{loading && <div>AI가 응답을 생성하고 있습니다...</div>}
|
||||
</div>
|
||||
|
||||
{/* 입력 영역 */}
|
||||
<input type="text" value={inputText} onChange={handleTextChange} placeholder="메시지를 입력하세요" style={{ width: 300 }} />
|
||||
<input type="file" multiple onChange={handleFilesChange} />
|
||||
{selectedFiles.length > 0 && (
|
||||
<ul style={{ marginTop: 4 }}>
|
||||
{selectedFiles.map((file, idx) => (
|
||||
<li key={idx}>
|
||||
{file.name} <button type="button" onClick={() => handleFileRemove(idx)}>삭제</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<button onClick={handleSend} disabled={loading}>전송</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
22
frontend/dev_chatbot/OCRService.js
Normal file
22
frontend/dev_chatbot/OCRService.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// OCRService.js – 브라우저에서 동작하는 간단 OCR 래퍼(Tesseract.js)
|
||||
// -----------------------------------------------------------------------------
|
||||
// 이미지 File 객체를 받아 한국어+영어 텍스트를 추출하여 반환한다.
|
||||
// 주의: 브라우저 워커 기반이므로 큰 이미지·다중 호출 시 성능 이슈가 있을 수 있다.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import Tesseract from 'tesseract.js';
|
||||
|
||||
/**
|
||||
* @param {File} imageFile - 이미지 파일 객체
|
||||
* @returns {Promise<string>} 추출 텍스트 (trim 처리)
|
||||
*/
|
||||
const OCRService = async (imageFile) => {
|
||||
try {
|
||||
const { data: { text } } = await Tesseract.recognize(imageFile, 'kor+eng');
|
||||
return text.trim();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export default OCRService;
|
||||
5
frontend/lims_text2sql/README.md
Normal file
5
frontend/lims_text2sql/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# LIMS Text2SQL 프론트엔드
|
||||
|
||||
이 폴더는 LIMS Text2SQL 엔진을 위한 전용 프론트엔드 자바스크립트/컴포넌트를 보관합니다.
|
||||
현재 메인 UI에서는 외부 서비스(iframe)로 연결하므로 파일이 비어 있습니다.
|
||||
필요 시 이곳에 컴포넌트를 추가하세요.
|
||||
5
frontend/research_qa/README.md
Normal file
5
frontend/research_qa/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 연구QA 프론트엔드
|
||||
|
||||
이 디렉터리에는 연구QA 엔진 전용 프론트엔드 코드가 들어갑니다.
|
||||
현재 `index.html` 의 iframe 접근 방식으로 동작하므로 별도 UI 컴포넌트는 필요하지 않습니다.
|
||||
향후 기능 고도화 시 이곳에 React/Vue 컴포넌트를 추가하세요.
|
||||
Reference in New Issue
Block a user