|
@@ -1,5 +1,12 @@
|
|
|
<script setup>
|
|
|
-import { ref, reactive, onMounted, watch, nextTick } from "vue";
|
|
|
+import {
|
|
|
+ ref,
|
|
|
+ reactive,
|
|
|
+ onMounted,
|
|
|
+ onBeforeUnmount,
|
|
|
+ watch,
|
|
|
+ nextTick,
|
|
|
+} from "vue";
|
|
|
import { useRoute } from "vue-router";
|
|
|
// VR
|
|
|
import "aframe";
|
|
@@ -138,12 +145,34 @@ let messages = ref([]);
|
|
|
watch(messages.value, (val) => {
|
|
|
console.log("messages", val);
|
|
|
// scrollToBottom();
|
|
|
- // 判斷最後一個值是否為文字訊息
|
|
|
- if (
|
|
|
- messages.value.length > 0 &&
|
|
|
- messages.value[messages.value.length - 1].label !== "brand"
|
|
|
- ) {
|
|
|
- scrollToBottom();
|
|
|
+ // // 判斷最後一個值是否為文字訊息
|
|
|
+ // if (
|
|
|
+ // messages.value.length > 0 &&
|
|
|
+ // messages.value[messages.value.length - 1].label !== "brand"
|
|
|
+ // )
|
|
|
+
|
|
|
+ if (messages.value.length > 0) {
|
|
|
+ let lastMessage = messages.value[messages.value.length - 1]; // 取得最後一個訊息
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ // 若訊息為按鈕選單則滑至底部
|
|
|
+ if (
|
|
|
+ lastMessage.label === "observation_deck" ||
|
|
|
+ lastMessage.label === "check" ||
|
|
|
+ lastMessage.label === "visit" ||
|
|
|
+ lastMessage.label === "navigation" ||
|
|
|
+ lastMessage.label === "garden_route" ||
|
|
|
+ lastMessage.label === "dining" ||
|
|
|
+ lastMessage.label === "shopping" ||
|
|
|
+ lastMessage.label === "shopping_brand" ||
|
|
|
+ lastMessage.label === "service" ||
|
|
|
+ lastMessage.label === "ticket"
|
|
|
+ ) {
|
|
|
+ scrollToBottom();
|
|
|
+ } else {
|
|
|
+ scrollToMessage();
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
}
|
|
|
updateMenuHeight();
|
|
|
});
|
|
@@ -153,30 +182,51 @@ let qaQuery = reactive([]);
|
|
|
|
|
|
const chatArea = ref(null); // 對話框
|
|
|
|
|
|
-// 滾動到對話框底部
|
|
|
-const scrollToBottom = () => {
|
|
|
- setTimeout(() => {
|
|
|
- // chatArea.value.scrollTop = chatArea.value.scrollHeight;
|
|
|
- console.log("chatArea.value", chatArea.value.scrollHeight);
|
|
|
-
|
|
|
- chatArea.value.scrollTo({
|
|
|
- top: chatArea.value.scrollHeight,
|
|
|
- behavior: "smooth",
|
|
|
- });
|
|
|
+// 捲軸滾動到使用者訊息底下第一則回覆
|
|
|
+const scrollToMessage = () => {
|
|
|
+ // 使用 nextTick 確保 DOM 已更新
|
|
|
+ nextTick(() => {
|
|
|
+ // 取得所有的 .message.message-out 元素
|
|
|
+ let messageOutElements = document.querySelectorAll(".message.message-out");
|
|
|
+
|
|
|
+ if (messageOutElements.length > 0) {
|
|
|
+ // 取得倒數第一個 .message.message-out
|
|
|
+ const lastMessageOut = messageOutElements[messageOutElements.length - 1];
|
|
|
+
|
|
|
+ // 取得該元素後的第一個 .message-content
|
|
|
+ const nextMessageContent =
|
|
|
+ lastMessageOut.parentElement.nextElementSibling;
|
|
|
+
|
|
|
+ if (
|
|
|
+ nextMessageContent &&
|
|
|
+ nextMessageContent.classList.contains("message-content")
|
|
|
+ ) {
|
|
|
+ // 取得該元素的滾動高度
|
|
|
+ const scrollTop = nextMessageContent.offsetTop;
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ // chatArea.value.scrollTop = scrollTop;
|
|
|
+
|
|
|
+ chatArea.value.scrollTo({
|
|
|
+ top: scrollTop - 45,
|
|
|
+ behavior: "smooth",
|
|
|
+ });
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
|
|
|
- // window.scrollTo({
|
|
|
- // top: document.body.scrollHeight,
|
|
|
- // behavior: "smooth", // 平滑滾動
|
|
|
- // });
|
|
|
- }, 100);
|
|
|
-
|
|
|
- // let list = messages.value;
|
|
|
- // const item = list[list.length - 1];
|
|
|
- // if (item.label === "text") {
|
|
|
- // setTimeout(() => {
|
|
|
- // chatArea.value.scrollTop = chatArea.value.scrollHeight;
|
|
|
- // }, 100);
|
|
|
- // }
|
|
|
+// 捲軸滾動到對話框底部
|
|
|
+const scrollToBottom = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ chatArea.value.scrollTo({
|
|
|
+ top: chatArea.value.scrollHeight,
|
|
|
+ behavior: "smooth",
|
|
|
+ });
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
// 對話選項(按鈕)
|
|
@@ -282,15 +332,12 @@ async function sendMessage(type = "") {
|
|
|
|
|
|
response.data.data.map((item) => {
|
|
|
if (item.type === "button") {
|
|
|
- console.log("按鈕");
|
|
|
info.buttonList.push(item);
|
|
|
labelContent = "ticket";
|
|
|
} else if (item.type === "ticket") {
|
|
|
- console.log("票");
|
|
|
info.ticketList.push(item);
|
|
|
labelContent = "ticket";
|
|
|
} else if (item.type === "brand") {
|
|
|
- console.log("品牌");
|
|
|
info.ticketList.push(item);
|
|
|
labelContent = "brand";
|
|
|
}
|
|
@@ -491,6 +538,8 @@ function chooseLang(lang) {
|
|
|
sources = videoSourcesEn.value;
|
|
|
} else if (language === "ja-jp") {
|
|
|
sources = videoSourcesJp.value;
|
|
|
+ } else if (language === "ko-kr") {
|
|
|
+ sources = videoSourcesKo.value;
|
|
|
} else {
|
|
|
sources = videoSources.value;
|
|
|
}
|
|
@@ -1488,6 +1537,7 @@ function assignMapImg(item) {
|
|
|
const videoSources = ref([]); // 開場白影片(中)
|
|
|
const videoSourcesEn = ref([]); // 開場白影片(英)
|
|
|
const videoSourcesJp = ref([]); // 開場白影片(日)
|
|
|
+const videoSourcesKo = ref([]); // 開場白影片(韓)
|
|
|
const videoMuteSources = ref([]); // 點頭影片(靜音)
|
|
|
const videoSpeakSources = ref([]); // 動嘴型影片
|
|
|
|
|
@@ -1510,6 +1560,7 @@ const loadVideoSources = async () => {
|
|
|
"https://cmm.ai/101-video/start_en_2.mp4",
|
|
|
];
|
|
|
videoSourcesJp.value = ["https://cmm.ai/101-video/start_jp_2.mp4"];
|
|
|
+ videoSourcesKo.value = ["https://cmm.ai/101-video/start_ko_2.mp4"];
|
|
|
videoMuteSources.value = [
|
|
|
// "https://cmm.ai/101-ai-chatbot-new/video/mute_1.mp4",
|
|
|
"https://cmm.ai/101-video/mute_2.mp4",
|
|
@@ -1552,6 +1603,8 @@ async function selectCategory(value, index) {
|
|
|
sources = videoSourcesEn.value;
|
|
|
} else if (lang === "jp") {
|
|
|
sources = videoSourcesJp.value;
|
|
|
+ } else if (lang === "ko") {
|
|
|
+ sources = videoSourcesKo.value;
|
|
|
}
|
|
|
|
|
|
const randomIndex = Math.floor(Math.random() * sources.length);
|
|
@@ -1601,6 +1654,12 @@ async function selectCategory(value, index) {
|
|
|
// });
|
|
|
funFilterDialog.value = true;
|
|
|
} else if (value === "秘境花園觀景台") {
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: t("observation_deck"),
|
|
|
+ });
|
|
|
+
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
|
author: "ai",
|
|
@@ -1615,6 +1674,12 @@ async function selectCategory(value, index) {
|
|
|
} else if (value === "位置導引") {
|
|
|
assignLocationText.value = "";
|
|
|
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: t("location_guide"),
|
|
|
+ });
|
|
|
+
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
|
author: "ai",
|
|
@@ -1642,6 +1707,12 @@ async function selectCategory(value, index) {
|
|
|
} else if (value === "美食/伴手禮") {
|
|
|
// getAd("美食伴手禮");
|
|
|
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: t("food_souvenirs"),
|
|
|
+ });
|
|
|
+
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
|
author: "ai",
|
|
@@ -1655,6 +1726,11 @@ async function selectCategory(value, index) {
|
|
|
});
|
|
|
} else if (value === "購物及優惠") {
|
|
|
// getAd("購物及優惠");
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: t("shopping_discounts"),
|
|
|
+ });
|
|
|
|
|
|
messages.value.push({
|
|
|
label: "shopping",
|
|
@@ -1662,6 +1738,12 @@ async function selectCategory(value, index) {
|
|
|
body: shoppingList,
|
|
|
});
|
|
|
} else if (value === "服務資訊") {
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: t("service_information"),
|
|
|
+ });
|
|
|
+
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
|
author: "ai",
|
|
@@ -1829,6 +1911,12 @@ const menuList = reactive([
|
|
|
async function findBrand(value) {
|
|
|
console.log("findBrand", value);
|
|
|
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: value,
|
|
|
+ });
|
|
|
+
|
|
|
if (value === "館外店家") {
|
|
|
value = "館外";
|
|
|
}
|
|
@@ -1849,7 +1937,7 @@ async function findBrand(value) {
|
|
|
assignCategoryIndex.value = null;
|
|
|
assignCategory.value = "";
|
|
|
|
|
|
- scrollToLastMessage();
|
|
|
+ // scrollToLastMessage();
|
|
|
} catch (error) {
|
|
|
console.log("error", error);
|
|
|
}
|
|
@@ -1885,6 +1973,12 @@ async function getStaticTickets(type) {
|
|
|
function handleObservationDialog(value) {
|
|
|
console.log("value", value);
|
|
|
|
|
|
+ messages.value.push({
|
|
|
+ label: "text",
|
|
|
+ author: "user",
|
|
|
+ body: value,
|
|
|
+ });
|
|
|
+
|
|
|
if (value === "線上購票") {
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
@@ -2013,10 +2107,16 @@ function handleShoppingDialog(value) {
|
|
|
});
|
|
|
|
|
|
messages.value.push({
|
|
|
- label: "text",
|
|
|
+ label: "tourist_card",
|
|
|
author: "ai",
|
|
|
body: t("shopping_discounts_info.tourist_card.content"),
|
|
|
});
|
|
|
+
|
|
|
+ // messages.value.push({
|
|
|
+ // label: "text",
|
|
|
+ // author: "ai",
|
|
|
+ // body: t("shopping_discounts_info.tourist_card.content"),
|
|
|
+ // });
|
|
|
} else if (value === "參觀資訊") {
|
|
|
messages.value.push({
|
|
|
label: "text",
|
|
@@ -2216,9 +2316,17 @@ async function handleAudioToText() {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
- // let url = `http://172.104.93.163:9880/whisper/${audioLang}/`;
|
|
|
- // let url = `https://cmm.ai:9001/whisper/${audioLang}/`;
|
|
|
- let url = `https://cmm.ai:9001/gcp/speech-to-text?language_code=${audioLang}`;
|
|
|
+ let url;
|
|
|
+
|
|
|
+ if (lang === "ko-kr") {
|
|
|
+ // 韓文使用 azure
|
|
|
+ url = `https://cmm.ai:9001/azure/speech-to-text?language_code=${audioLang}`;
|
|
|
+ } else {
|
|
|
+ // 其他語言使用 gcp
|
|
|
+ url = `https://cmm.ai:9001/gcp/speech-to-text?language_code=${audioLang}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // let url = `https://cmm.ai:9001/gcp/speech-to-text?language_code=${audioLang}`;
|
|
|
|
|
|
const formData = new FormData();
|
|
|
formData.append("file", audioFile.value);
|
|
@@ -2229,11 +2337,19 @@ async function handleAudioToText() {
|
|
|
console.log("語音轉文字 response", response);
|
|
|
// showAnchor.value = false; // 關閉主播視窗
|
|
|
|
|
|
- userMessage.value = response.data[0];
|
|
|
+ let message;
|
|
|
+
|
|
|
+ if (lang === "ko-kr") {
|
|
|
+ message = response.data;
|
|
|
+ } else {
|
|
|
+ message = response.data[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ userMessage.value = message;
|
|
|
|
|
|
// handleTTS(userMessage.value); // 取得語音回覆
|
|
|
|
|
|
- if (response.data[0] && response.data[0] !== "") {
|
|
|
+ if (message && message !== "") {
|
|
|
sendMessage(); // 傳送使用者訊息
|
|
|
} else {
|
|
|
if (showAnchor.value) {
|
|
@@ -2317,6 +2433,18 @@ function recStop() {
|
|
|
// rec.close(); // 釋放錄音資源 (若不釋放系統或瀏覽器將持續提示在錄音中)
|
|
|
// rec = null;
|
|
|
|
|
|
+ // // 韓文轉 wav 格式,其他語言轉 mp3 格式
|
|
|
+ // let lang = getLang();
|
|
|
+ // if (lang === "ko") {
|
|
|
+ // audioFile.value = new File([blob], "recording.wav", {
|
|
|
+ // type: "audio/wav",
|
|
|
+ // });
|
|
|
+ // } else {
|
|
|
+ // audioFile.value = new File([blob], "recording.mp3", {
|
|
|
+ // type: "audio/mp3",
|
|
|
+ // });
|
|
|
+ // }
|
|
|
+
|
|
|
// 將 Blob 轉換為 File 對象
|
|
|
audioFile.value = new File([blob], "recording.mp3", {
|
|
|
type: "audio/mp3",
|
|
@@ -2395,15 +2523,12 @@ function setInfo(data) {
|
|
|
|
|
|
data.map((item) => {
|
|
|
if (item.type === "button") {
|
|
|
- console.log("按鈕");
|
|
|
info.buttonList.push(item);
|
|
|
labelContent = "ticket";
|
|
|
} else if (item.type === "ticket") {
|
|
|
- console.log("票");
|
|
|
info.ticketList.push(item);
|
|
|
labelContent = "ticket";
|
|
|
} else if (item.type === "brand") {
|
|
|
- console.log("品牌");
|
|
|
info.ticketList.push(item);
|
|
|
labelContent = "brand";
|
|
|
}
|
|
@@ -2541,26 +2666,22 @@ async function messageNotInCache(question, answer) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-const scrollToLastMessage = () => {
|
|
|
- nextTick(() => {
|
|
|
- console.log(">>>");
|
|
|
-
|
|
|
- const lastMessage = chatArea.value?.querySelector(
|
|
|
- ".message-content:last-child"
|
|
|
- );
|
|
|
-
|
|
|
- console.log("lastMessage", lastMessage);
|
|
|
-
|
|
|
- if (lastMessage) {
|
|
|
- const scrollTop = lastMessage.offsetTop;
|
|
|
- console.log("捲軸高度:", scrollTop);
|
|
|
- setTimeout(() => {
|
|
|
- chatArea.value.scrollTop = lastMessage.offsetTop - 45;
|
|
|
- }, 500);
|
|
|
- console.log("chatArea.value.scrollTop", chatArea.value.scrollTop);
|
|
|
- }
|
|
|
- });
|
|
|
-};
|
|
|
+// // 捲軸捲動到最後一個訊息
|
|
|
+// const scrollToLastMessage = () => {
|
|
|
+// nextTick(() => {
|
|
|
+// const lastMessage = chatArea.value?.querySelector(
|
|
|
+// ".message-content:last-child"
|
|
|
+// );
|
|
|
+
|
|
|
+// if (lastMessage) {
|
|
|
+// const scrollTop = lastMessage.offsetTop;
|
|
|
+// console.log("捲軸高度:", scrollTop);
|
|
|
+// setTimeout(() => {
|
|
|
+// chatArea.value.scrollTop = lastMessage.offsetTop - 45;
|
|
|
+// }, 500);
|
|
|
+// }
|
|
|
+// });
|
|
|
+// };
|
|
|
|
|
|
// 判斷輸入框狀態
|
|
|
let inputFocus = ref(false);
|
|
@@ -2572,6 +2693,55 @@ const handleFocus = () => {
|
|
|
const handleBlur = () => {
|
|
|
inputFocus.value = false;
|
|
|
};
|
|
|
+
|
|
|
+// 判斷影片區塊背景色
|
|
|
+function videoSrcBg() {
|
|
|
+ // 四種語言開場白 & 純點頭 & 對嘴影片
|
|
|
+ if (
|
|
|
+ videoSrc.value === videoSources.value[0] ||
|
|
|
+ videoSrc.value === videoSourcesEn.value[0] ||
|
|
|
+ videoSrc.value === videoSourcesJp.value[0] ||
|
|
|
+ videoSrc.value === videoSourcesKo.value[0] ||
|
|
|
+ videoSrc.value[0] === videoMuteSources.value[0] ||
|
|
|
+ videoSrc.value === videoSpeakSources.value[0]
|
|
|
+ ) {
|
|
|
+ return "#cfba93";
|
|
|
+ } else {
|
|
|
+ return "#cab78e";
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 時間內無動作需 reload
|
|
|
+let timeout;
|
|
|
+
|
|
|
+const resetTimeout = () => {
|
|
|
+ clearTimeout(timeout);
|
|
|
+ timeout = setTimeout(() => {
|
|
|
+ window.location.reload(); // 重新整理網頁
|
|
|
+ }, 5 * 60 * 1000); // 5 分鐘 = 5 * 60 * 1000 毫秒
|
|
|
+};
|
|
|
+
|
|
|
+const startInactivityTimer = () => {
|
|
|
+ console.log("startInactivityTimer", timeout);
|
|
|
+
|
|
|
+ window.addEventListener("mousemove", resetTimeout);
|
|
|
+ window.addEventListener("keydown", resetTimeout);
|
|
|
+ resetTimeout(); // 初始化計時器
|
|
|
+};
|
|
|
+
|
|
|
+const stopInactivityTimer = () => {
|
|
|
+ clearTimeout(timeout);
|
|
|
+ window.removeEventListener("mousemove", resetTimeout);
|
|
|
+ window.removeEventListener("keydown", resetTimeout);
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ startInactivityTimer();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ stopInactivityTimer();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
@@ -2589,7 +2759,12 @@ const handleBlur = () => {
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="main-containar">
|
|
|
- <div class="video-content">
|
|
|
+ <div
|
|
|
+ class="video-content"
|
|
|
+ :style="{
|
|
|
+ backgroundColor: videoSrcBg(),
|
|
|
+ }"
|
|
|
+ >
|
|
|
<!-- 語言選單 -->
|
|
|
<div class="lang-select">
|
|
|
<v-select
|
|
@@ -3553,6 +3728,37 @@ const handleBlur = () => {
|
|
|
<span class="price-item">{{ message.body.price }}</span>
|
|
|
</section>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 國際貴賓卡專屬禮遇 -->
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="message.label === 'tourist_card'"
|
|
|
+ class="message animate__animated"
|
|
|
+ :class="{
|
|
|
+ 'message-out': message.author === 'user',
|
|
|
+ 'message-in': message.author !== 'user',
|
|
|
+ animate__fadeInRight: message.author === 'user',
|
|
|
+ animate__fadeInLeft: message.author !== 'user',
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <p v-html="t('shopping_discounts_info.tourist_card.content')"></p>
|
|
|
+ <button
|
|
|
+ @click="
|
|
|
+ openQrcodeDialog(
|
|
|
+ 'https://stage.taipei101mall.com.tw/join-member/AIsystem'
|
|
|
+ )
|
|
|
+ "
|
|
|
+ class="ar-link mt-5 mb-3"
|
|
|
+ >
|
|
|
+ {{ t("shopping_discounts_info.tourist_card.apply") }}
|
|
|
+ </button>
|
|
|
+ <!-- <a
|
|
|
+ href="https://stage.taipei101mall.com.tw/join-member/AIsystem"
|
|
|
+ class="ar-link mb-3"
|
|
|
+ target="_blank"
|
|
|
+ >立即線上申辦</a
|
|
|
+ > -->
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
@@ -3897,7 +4103,8 @@ const handleBlur = () => {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
transform: scale(1);
|
|
|
- background-color: #cab78e;
|
|
|
+ // background-color: #cfba93;
|
|
|
+ // background-color: #cab78e;
|
|
|
// background-color: #d1bf99;
|
|
|
// background-color: var(--sub-color);
|
|
|
|
|
@@ -4572,6 +4779,7 @@ const handleBlur = () => {
|
|
|
background-color: var(--main-color);
|
|
|
border-radius: 5px;
|
|
|
text-decoration: none;
|
|
|
+ letter-spacing: 1px;
|
|
|
}
|
|
|
|
|
|
.ar-card {
|