챗봇 POC
-
챗봇 백앤드와 프런트앤드를 구현
- express 프로젝트 생성
Terminal window mkdir chatbot-servercd chatbot-server - 라이브러리 설치
- express
Terminal window pnpm add express cors body-parser - openai
Terminal window pnpm add openai
- express
- package.json 파일 수정
package.json {"type": "module","dependencies": {"body-parser": "^2.2.0","cors": "^2.8.5","express": "^5.1.0","openai": "^6.6.0"}} - app.js 파일 생성
Terminal window touch app.jsapp.js import express from "express";import bodyParser from "body-parser";import cors from "cors";const app = express();const port = 2000;app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(cors());app.get("/", (req, res) => {return res.json({ data: "success" });});app.listen(port, () => {console.log(`Example app listening on port ${port}`);}); - 모듈 작성
- 디렉토리 생성
Terminal window mkdir -p src/modules/openaicd src/modules/openai - 모듈 작성
Terminal window touch index.jssrc/modules/openai/index.js import OpenAI from "openai";const client = new OpenAI({organization: "<>",apiKey: "<>",});const systemMessage = {role: "system",content:"You are a Askbot. You are supposed to answer the questions asked by the users. Validate the prompts to be a question and it should not in approprite. Give funky responses",};export const getStreamingCompletion = async ({ userPrompt }) => {return client.chat.completions.create({model: "gpt-3.5-turbo",messages: [systemMessage, { role: "user", content: userPrompt }],stream: true,});};
- 디렉토리 생성
- 실행
Terminal window node app.js
- vite 프로젝트 생성
Terminal window pnpm create vite$ pnpm create vite|o Project name:| chatbot-client|o Select a framework:| React|o Select a variant:| TypeScript + SWC|o Use rolldown-vite (Experimental)?:| Yes|o Install with pnpm and start now?| Yes|o Scaffolding project in D:\study\chatbot-client...|o Installing dependencies with pnpm...Packages: +153++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++Progress: resolved 192, reused 159, downloaded 0, added 153, donedependencies:+ react 19.2.0+ react-dom 19.2.0+ vite <- rolldown-vite 7.1.14 (7.1.16 is available)devDependencies:+ @eslint/js 9.38.0+ @types/node 24.9.1+ @types/react 19.2.2+ @types/react-dom 19.2.2+ @vitejs/plugin-react-swc 4.1.0+ eslint 9.38.0+ eslint-plugin-react-hooks 5.2.0 (7.0.0 is available)+ eslint-plugin-react-refresh 0.4.24+ globals 16.4.0+ typescript 5.9.3+ typescript-eslint 8.46.2╭ Warning ─────────────────────────────────────────────────────────────────────╮│ ││ Ignored build scripts: @swc/core. ││ Run "pnpm approve-builds" to pick which dependencies should be allowed ││ to run scripts. ││ │╰──────────────────────────────────────────────────────────────────────────────╯Done in 3.4s using pnpm v10.17.1|o Starting dev server...> chatbot-client@0.0.0 dev D:\study\chatbot-client> vite[vite:react-swc] We recommend switching to `@vitejs/plugin-react` for improved performance as no swc plugins are used. More information at https://vite.dev/rolldownROLLDOWN-VITE v7.1.14 ready in 211 ms➜ Local: http://localhost:5173/➜ Network: use --host to expose➜ press h + enter to show help
프런트앤드에서 변경/추가된 파일 목록
Section titled “프런트앤드에서 변경/추가된 파일 목록”Directorysrc
Directoryassets
- lens.png
- loading.gif
- App.css
- App.tsx
- index.css
.app { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.1);}
.app-container { width: 1000px; max-width: 100%; padding: 0 20px; text-align: center;}
.spotlight__wrapper { border-radius: 12px; border: 1px solid #dfe1e5; margin: auto; max-width: 600px; background-color: #fff;}
.spotlight__wrapper:hover,.spotlight__wrapper:focus { background-color: #fff; box-shadow: 0 1px 6px rgb(32 33 36 / 28%); border-color: rgba(223, 225, 229, 0);}
.spotlight__input { display: block; height: 56px; width: 80%; border: 0; border-radius: 12px; outline: none; font-size: 1.2rem; color: #000; background-position: left 17px center; background-repeat: no-repeat; background-color: #fff; background-size: 3.5%; padding-left: 60px;}
.spotlight__input::placeholder { line-height: 1.5em;}
.spotlight__answer { min-height: 115px; line-height: 1.5em; letter-spacing: 0.1px; padding: 10px 30px; display: flex; align-items: center; justify-content: center;}
.spotlight__answer p::after { content: ""; width: 2px; height: 14px; position: relative; top: 2px; left: 2px; background: black; display: inline-block; animation: cursor-blink 1s steps(2) infinite;}
@keyframes cursor-blink { 0% { opacity: 0; }}import React, { useState, useEffect } from "react";import "./App.css";import lens from "./assets/lens.png";import loadingGif from "./assets/loading.gif";
function App() { const [prompt, updatePrompt] = useState<string>(); const [loading, setLoading] = useState<boolean>(false); const [answer, setAnswer] = useState<string>("");
useEffect(() => { if (prompt != null && prompt.trim() === "") { setAnswer(""); } }, [prompt]);
const sendPrompt = async (event: React.KeyboardEvent) => { if (event.key !== "Enter") { return; }
try { setLoading(true); setAnswer(""); const response = await fetch("http://localhost:2000/aiCompletion", { method: "post", headers: { Accept: "application/json, text/plain, */*", // indicates which files we are able to understand "Content-Type": "application/json", // indicates what the server actually sent }, body: JSON.stringify({ userPrompt: prompt }), // server is expecting JSON }); if (!response.ok || !response.body) { throw response.statusText; } const reader = response.body.getReader(); const decoder = new TextDecoder(); const loopRunner = true;
while (loopRunner) { const { value, done } = await reader.read(); if (done) { break; } const decodedChunk = decoder.decode(value, { stream: true }); setAnswer((answer) => answer + decodedChunk); } } catch (err) { console.error(err, "err"); } finally { setLoading(false); } };
return ( <div className="app"> <div className="app-container"> <div className="spotlight__wrapper"> <input type="text" className="spotlight__input" placeholder="Ask me anything..." disabled={loading} style={{ backgroundImage: loading ? `url(${loadingGif})` : `url(${lens})`, }} onChange={(e) => updatePrompt(e.target.value)} onKeyDown={(e) => sendPrompt(e)} /> <div className="spotlight__answer">{answer && <p>{answer}</p>}</div> </div> </div> </div> );}
export default App;html,body,#root { height: 100%; font-size: 14px; font-family: arial, sans-serif; margin: 0; background-color: #242424;}
