Feat: Add chatbot component
This commit is contained in:
parent
0c5ad07f0e
commit
8770fd9f38
@ -1,58 +1,11 @@
|
||||
// import axios from 'axios';
|
||||
|
||||
// const apiKey = import.meta.env.VITE_OPENAI_API_KEY;
|
||||
|
||||
// const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// export const sendMessageToChatGPT = async (message, retries = 3) => {
|
||||
// let attempt = 0;
|
||||
// const maxDelay = 32000; // 최대 지연 시간 (32초)
|
||||
|
||||
// while (attempt < retries) {
|
||||
// try {
|
||||
// console.log(apiKey)
|
||||
// const response = await axios.post(
|
||||
// 'https://api.openai.com/v1/chat/completions',
|
||||
// {
|
||||
// model: 'gpt-3.5-turbo', // 또는 다른 사용 가능한 모델로 변경
|
||||
// messages: [
|
||||
// { role: 'system', content: 'You are a tour guide for those who want to travel to Korea. Answer only questions related to your trip to Korea.' },
|
||||
// { role: 'user', content: message }
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${apiKey}`,
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
// return response.data.choices[0].message.content;
|
||||
// } catch (error) {
|
||||
// if (error.response && error.response.status === 429) {
|
||||
// attempt++;
|
||||
// const backoffTime = Math.min(1000 * 2 ** attempt, maxDelay); // 지수 백오프
|
||||
// console.warn(`Rate limit exceeded. Retrying in ${backoffTime / 1000} seconds...`);
|
||||
// await delay(backoffTime);
|
||||
// } else {
|
||||
// console.error('Error calling OpenAI API:', error);
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// throw new Error('Exceeded maximum retry attempts');
|
||||
// };
|
||||
|
||||
import { localAxios } from '@/utils/http-commons';
|
||||
|
||||
const local = localAxios;
|
||||
|
||||
const sendMessageToChatGPT = async (message) => {
|
||||
try {
|
||||
console.log(message)
|
||||
const response = await local.post('/api/chatgpt/message', { message });
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error sending message to ChatGPT:', error);
|
||||
|
54
src/components/chatbot/ChatBotButton.vue
Normal file
54
src/components/chatbot/ChatBotButton.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import FilledButton from '../common/FilledButton.vue';
|
||||
import TravelChatBot from './TravelChatBot.vue';
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
function toggleChatbot() {
|
||||
isOpen.value = !isOpen.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FilledButton class="chatbot-button" @click="toggleChatbot">채팅</FilledButton>
|
||||
<div v-if="isOpen" class="chatbot-wrapper">
|
||||
<TravelChatBot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chatbot-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 20px, 0) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.chatbot-wrapper {
|
||||
position: absolute;
|
||||
bottom: 104px;
|
||||
right: 20px;
|
||||
background-color: var(--color-background);
|
||||
width: 390px;
|
||||
height: calc(100% - 124px);
|
||||
max-height: 600px;
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
animation: show 0.35s ease;
|
||||
transform-origin: bottom right;
|
||||
z-index: 9;
|
||||
}
|
||||
</style>
|
@ -1,183 +1,160 @@
|
||||
<!-- <template>
|
||||
<div class="chat-container">
|
||||
<div class="messages">
|
||||
<div v-for="(msg, index) in messages" :key="index" :class="msg.role">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<input
|
||||
v-model="userInput"
|
||||
@keyup.enter="handleSend"
|
||||
type="text"
|
||||
placeholder="Type your travel query..."
|
||||
/>
|
||||
<button @click="handleSend">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import sendMessageToChatGPT from '@/api/chatbot';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import SearchBox from '../common/SearchBox.vue';
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { sendMessageToChatGPT } from "@/api/chatbot";
|
||||
const userInput = ref('');
|
||||
const messages = ref([]);
|
||||
const scrollRef = ref(null);
|
||||
const pending = ref(false);
|
||||
|
||||
export default {
|
||||
name: 'TravelChatBot',
|
||||
setup() {
|
||||
const messages = ref([]);
|
||||
const userInput = ref('');
|
||||
function handleSubmit() {
|
||||
if (!userInput.value?.trim()) return;
|
||||
|
||||
const handleSend = async () => {
|
||||
if (userInput.value.trim() === '') return;
|
||||
|
||||
const userMessage = { role: 'user', content: userInput.value };
|
||||
console.log(userMessage)
|
||||
messages.value.push(userMessage);
|
||||
messages.value.push({ role: 'user', content: userInput.value });
|
||||
pending.value = true;
|
||||
nextTick().then(() => {
|
||||
scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
});
|
||||
sendMessageToChatGPT(userInput.value)
|
||||
.then((response) => {
|
||||
pending.value = false;
|
||||
messages.value.push({ role: 'assistant', content: response });
|
||||
})
|
||||
.then(() => nextTick())
|
||||
.then(() => {
|
||||
scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
});
|
||||
|
||||
userInput.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
const botMessageContent = await sendMessageToChatGPT(userMessage.content);
|
||||
const botMessage = { role: 'assistant', content: botMessageContent };
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<div class="messages" ref="scrollRef">
|
||||
<div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
<div v-if="pending" class="message assistant"><div class="skeleton"></div></div>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<SearchBox v-model="userInput" :onSubmit="handleSubmit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="3,11 22,2 13,21 11,13 3,11" />
|
||||
</svg>
|
||||
</SearchBox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
messages.value.push(botMessage);
|
||||
};
|
||||
<style scoped>
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
userInput,
|
||||
handleSend,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@keyframes skeleton {
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.chat-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.messages .user {
|
||||
text-align: right;
|
||||
margin: 10px 0;
|
||||
color: blue;
|
||||
}
|
||||
.chat-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-text-secondary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.messages .assistant {
|
||||
text-align: left;
|
||||
margin: 10px 0;
|
||||
color: green;
|
||||
}
|
||||
.icon {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
.messages {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<div class="chat-box">
|
||||
<div class="messages">
|
||||
<div v-for="(msg, index) in messages" :key="index" :class="['message', msg.role]">
|
||||
<span>{{ msg.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input v-model="userInput" @keyup.enter="sendMessage" placeholder="Ask me anything about travel" />
|
||||
<button @click="sendMessage">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sendMessageToChatGPT from "@/api/chatbot";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
messages: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async sendMessage() {
|
||||
if (this.userInput.trim() === '') return;
|
||||
|
||||
this.messages.push({ role: 'user', content: this.userInput });
|
||||
|
||||
console.log(this.userInput)
|
||||
const response = await sendMessageToChatGPT(this.userInput);
|
||||
|
||||
this.messages.push({ role: 'assistant', content: response });
|
||||
|
||||
this.userInput = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
width: 400px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.messages {
|
||||
max-height: 300px;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
height: 100%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
.messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.messages::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-text-secondary);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
animation: slideUp 0.25s ease-out;
|
||||
}
|
||||
|
||||
.user {
|
||||
background-color: #e0f7fa;
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
var(--color-border) 30%,
|
||||
var(--color-background-soft) 38%,
|
||||
var(--color-background-soft) 42%,
|
||||
var(--color-border) 50%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
background-position: 100% 0;
|
||||
animation: skeleton 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.user {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.assistant {
|
||||
background-color: #fff9c4;
|
||||
}
|
||||
</style>
|
||||
.assistant {
|
||||
background-color: var(--color-background-soft);
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
justify-self: flex-end;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding-right: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -40,12 +40,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="map" ref="mapDiv" />
|
||||
<div id="map" ref="mapDiv"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#map {
|
||||
width: calc(100% - 400px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -11,21 +11,10 @@ const model = defineModel();
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="onSubmit" class="search-box">
|
||||
<input type="text" :name="name" v-model.lazy="model" placeholder="검색어를 입력하세요" />
|
||||
<input type="text" :name="name" v-model="model" placeholder="검색어를 입력하세요" />
|
||||
|
||||
<TextButton @click="onSubmit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-search"
|
||||
viewBox="0 0 16 16"
|
||||
part="svg"
|
||||
>
|
||||
<path
|
||||
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<slot></slot>
|
||||
</TextButton>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -32,6 +32,7 @@ watch(sido, ({ sidoCode }) => {
|
||||
axios.get(`//localhost:8000/area/gugun?sidoCode=${sidoCode}`).then(({ data }) => {
|
||||
gugunList.value = [{ gugunCode: 0, gugunName: '전체' }, ...data];
|
||||
});
|
||||
console.log(keyword.value);
|
||||
});
|
||||
|
||||
if (sidoList.value.length === 0) {
|
||||
@ -48,9 +49,14 @@ function handleSubmit() {
|
||||
const contentTypeQuery = `contentTypeId=${contentType.value?.value ?? 0}`;
|
||||
const queryString = [areaQuery, keywordQuery, contentTypeQuery].join('&');
|
||||
|
||||
console.log(queryString);
|
||||
setQueryString(queryString);
|
||||
search();
|
||||
}
|
||||
|
||||
watch(keyword, () => {
|
||||
console.log('change');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -71,7 +77,21 @@ function handleSubmit() {
|
||||
optionName="name"
|
||||
/>
|
||||
</nav>
|
||||
<SearchBox name="keyword" v-model.lazy="keyword" :onSubmit="handleSubmit" />
|
||||
<SearchBox name="keyword" v-model="keyword" :onSubmit="handleSubmit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-search"
|
||||
viewBox="0 0 16 16"
|
||||
part="svg"
|
||||
>
|
||||
<path
|
||||
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</SearchBox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,31 +1,31 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { registMember } from "@/api/member";
|
||||
import { useRouter } from "vue-router";
|
||||
import { reactive, ref } from 'vue';
|
||||
import { registMember } from '@/api/member';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const email_id = ref("")
|
||||
const email_domain = ref("")
|
||||
const email_id = ref('');
|
||||
const email_domain = ref('');
|
||||
|
||||
const member = ref({
|
||||
id: "",
|
||||
name: "",
|
||||
password: "",
|
||||
email: email_id.value + "@" + email_domain.value,
|
||||
})
|
||||
const member = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
password: '',
|
||||
email: email_id.value + '@' + email_domain.value,
|
||||
});
|
||||
|
||||
function onSubmit() {
|
||||
member.value.email = email_id.value + "@" + email_domain.value
|
||||
console.log(member.value)
|
||||
member.value.email = email_id.value + '@' + email_domain.value;
|
||||
console.log(member.value);
|
||||
registMember(
|
||||
member.value,
|
||||
(response) => {
|
||||
if (response.status == 200) console.log("회원가입 성공!")
|
||||
router.push({ name : "user-login"})
|
||||
if (response.status == 200) console.log('회원가입 성공!');
|
||||
router.push({ name: 'user-login' });
|
||||
},
|
||||
(error) => console.error(error)
|
||||
)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -41,15 +41,20 @@ function onSubmit() {
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">이름 : </label>
|
||||
<input type="text" class="form-control" placeholder="이름..." v-model="member.name"/>
|
||||
<input type="text" class="form-control" placeholder="이름..." v-model="member.name" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userid" class="form-label">아이디 : </label>
|
||||
<input type="text" class="form-control" placeholder="아이디..." v-model="member.id"/>
|
||||
<input type="text" class="form-control" placeholder="아이디..." v-model="member.id" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="userpwd" class="form-label">비밀번호 : </label>
|
||||
<input type="text" class="form-control" placeholder="비밀번호..." v-model="member.password"/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="비밀번호..."
|
||||
v-model="member.password"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pwdcheck" class="form-label">비밀번호확인 : </label>
|
||||
@ -58,7 +63,12 @@ function onSubmit() {
|
||||
<div class="mb-3">
|
||||
<label for="emailid" class="form-label">이메일 : </label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="이메일아이디" v-model="email_id"/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="이메일아이디"
|
||||
v-model="email_id"
|
||||
/>
|
||||
<span class="input-group-text">@</span>
|
||||
<select class="form-select" aria-label="이메일 도메인 선택" v-model="email_domain">
|
||||
<option selected>선택</option>
|
||||
@ -70,7 +80,9 @@ function onSubmit() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto text-center">
|
||||
<button type="button" class="btn btn-outline-primary mb-3" @click="onSubmit">회원가입</button>
|
||||
<button type="button" class="btn btn-outline-primary mb-3" @click="onSubmit">
|
||||
회원가입
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success ms-1 mb-3">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -2,7 +2,7 @@ import './assets/main.css';
|
||||
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
@ -15,7 +15,6 @@ pinia.use(piniaPluginPersistedstate);
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
|
||||
// app.mount('#app');
|
||||
router.isReady().then(() => {
|
||||
app.mount("#app");
|
||||
});
|
||||
app.mount('#app');
|
||||
});
|
||||
|
@ -1,24 +1,31 @@
|
||||
<script setup>
|
||||
import AppHeader from '@/components/AppHeader.vue';
|
||||
import ChatBotButton from '@/components/chatbot/ChatBotButton.vue';
|
||||
import KakaoMap from '@/components/common/KakaoMap.vue';
|
||||
import SideBar from '@/components/search/SideBar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppHeader />
|
||||
<main>
|
||||
<div class="view-wrapper">
|
||||
<SideBar />
|
||||
<main>
|
||||
<KakaoMap />
|
||||
<ChatBotButton />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
.view-wrapper {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user