Fix chat box and make it responsive
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import
|
// import
|
||||||
|
|
||||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
|
||||||
import useGetChat from "~/composables/api/chat/useGetChat";
|
import useGetChat from "~/composables/api/chat/useGetChat";
|
||||||
import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
|
||||||
import { useIsMutating } from "@tanstack/vue-query";
|
import { useIsMutating } from "@tanstack/vue-query";
|
||||||
@@ -20,6 +19,8 @@ const { isLoggedIn } = useAuth();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const id = route.params.id as string | number;
|
const id = route.params.id as string | number;
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 480px)");
|
||||||
|
|
||||||
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
|
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const chatContainerEl = ref<HTMLElement | null>(null);
|
const chatContainerEl = ref<HTMLElement | null>(null);
|
||||||
@@ -31,26 +32,24 @@ const {
|
|||||||
isPending: isChatPending,
|
isPending: isChatPending,
|
||||||
isFetchingNextPage: isNextChatPagePending,
|
isFetchingNextPage: isNextChatPagePending,
|
||||||
hasNextPage: hasMoreChat,
|
hasNextPage: hasMoreChat,
|
||||||
fetchNextPage: loadMoreChat
|
fetchNextPage: loadMoreChat,
|
||||||
} = useGetChat(id, isOpen);
|
} = useGetChat(id, isOpen);
|
||||||
const isCreateMessagePending = useIsMutating({
|
const isCreateMessagePending = useIsMutating({
|
||||||
mutationKey: [MUTATION_KEYS.create_chat]
|
mutationKey: [MUTATION_KEYS.create_chat],
|
||||||
});
|
});
|
||||||
|
|
||||||
const canLoadMoreChat = ref(false);
|
const canLoadMoreChat = ref(false);
|
||||||
|
|
||||||
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
const isChatScrollLocked = useScrollLock(chatContainerEl);
|
||||||
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
|
||||||
behavior: "smooth"
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
|
|
||||||
useInfiniteScroll(
|
useInfiniteScroll(
|
||||||
chatContainerEl,
|
chatContainerEl,
|
||||||
async () => {
|
async () => {
|
||||||
if (hasMoreChat.value && !isChatPending.value) {
|
if (hasMoreChat.value && !isChatPending.value) {
|
||||||
lastMessageBeforeUpdate.value = chatMessages.value
|
lastMessageBeforeUpdate.value = chatMessages.value ? chatMessages.value[0].id : 0;
|
||||||
? chatMessages.value[0].id
|
|
||||||
: 0;
|
|
||||||
await loadMoreChat();
|
await loadMoreChat();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -58,7 +57,7 @@ useInfiniteScroll(
|
|||||||
distance: 10,
|
distance: 10,
|
||||||
direction: "top",
|
direction: "top",
|
||||||
throttle: 1000,
|
throttle: 1000,
|
||||||
canLoadMore: () => canLoadMoreChat.value
|
canLoadMore: () => canLoadMoreChat.value,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,8 +102,7 @@ watch(
|
|||||||
`#message-container-${lastMessageBeforeUpdate.value}`
|
`#message-container-${lastMessageBeforeUpdate.value}`
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
lastChatMessageEl?.scrollIntoView();
|
lastChatMessageEl?.scrollIntoView();
|
||||||
chatContainerEl.value!.scrollTop =
|
chatContainerEl.value!.scrollTop = chatContainerEl.value!.scrollTop + scrollTopOld;
|
||||||
chatContainerEl.value!.scrollTop + scrollTopOld;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,28 +119,31 @@ whenever(
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
once: true
|
once: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Transition name="fade-right-to-left">
|
<Transition :name="isMobile ? 'fade-down' : 'fade-right-to-left'">
|
||||||
<div
|
<div
|
||||||
v-if="isOpen"
|
v-if="isOpen"
|
||||||
class="fixed z-50 right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
|
class="fixed z-50 max-xs:inset-0 xs:right-8 xs:bottom-8 w-full xs:w-[450px] transition-all duration-500 overflow-hidden h-svh xs:h-[700px] xs:rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
|
||||||
>
|
>
|
||||||
<CloseButton :disabled="!!isCreateMessagePending" />
|
<CloseButton :disabled="!!isCreateMessagePending" />
|
||||||
|
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<Transition name="zoom" mode="out-in">
|
<Transition
|
||||||
|
name="zoom"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isChatPending"
|
v-if="!isChatPending"
|
||||||
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
class="p-4.5 h-full flex flex-col justify-between gap-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:style="{
|
:style="{
|
||||||
maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)'
|
maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)',
|
||||||
}"
|
}"
|
||||||
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
|
||||||
ref="chatContainerEl"
|
ref="chatContainerEl"
|
||||||
@@ -183,7 +184,11 @@ whenever(
|
|||||||
v-else
|
v-else
|
||||||
class="w-full h-full flex items-center justify-center absolute inset-0"
|
class="w-full h-full flex items-center justify-center absolute inset-0"
|
||||||
>
|
>
|
||||||
<AiLoading />
|
<NuxtImg
|
||||||
|
class="size-[250px] drop-shadow-2xl"
|
||||||
|
src="/img/heymlz/heymlz-small-idle.gif"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
@@ -191,12 +196,15 @@ whenever(
|
|||||||
class="text-black p-6 size-full flex justify-center items-center flex-col"
|
class="text-black p-6 size-full flex justify-center items-center flex-col"
|
||||||
v-else
|
v-else
|
||||||
>
|
>
|
||||||
<NuxtImg class="size-[250px]" src="/img/heymlz/heymlz-loading-1.gif" alt="" />
|
<NuxtImg
|
||||||
|
class="size-[250px] drop-shadow-2xl"
|
||||||
|
src="/img/heymlz/heymlz-small-idle.gif"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
<div class="flex flex-col gap-4 items-center">
|
<div class="flex flex-col gap-4 items-center">
|
||||||
<span class="text-center typo-p-xl font-bold">سلام دوست عزیز!</span>
|
<span class="text-center typo-p-xl font-bold">سلام دوست عزیز!</span>
|
||||||
<p class="text-center typo-p-md">
|
<p class="text-center typo-p-md">
|
||||||
من میتونم هر سوالی رو درمورد این محصول جواب بدم
|
من میتونم هر سوالی رو درمورد این محصول جواب بدم اگه میخوای شروع کنیم روی دکمه زیر کلیک کن
|
||||||
اگه میخوای شروع کنیم روی دکمه زیر کلیک کن
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink to="/signin">
|
<NuxtLink to="/signin">
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// types
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
showChatButton: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
const isMobile = useMediaQuery("(max-width: 480px)");
|
||||||
|
const isWindowScrollLocked = useScrollLock(window);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
|
|
||||||
@@ -14,19 +26,29 @@ provide("isOpen", {
|
|||||||
closeChat,
|
closeChat,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// watches
|
||||||
|
|
||||||
|
watch([isMobile, isOpen], ([isMobile, isOpen]) => {
|
||||||
|
isWindowScrollLocked.value = isMobile && isOpen;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<Transition
|
||||||
v-if="!isOpen"
|
name="fade"
|
||||||
@click="isOpen = !isOpen"
|
:duration="150"
|
||||||
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
|
|
||||||
>
|
>
|
||||||
<Icon
|
<button
|
||||||
name="streamline:artificial-intelligence-spark"
|
v-if="showChatButton && !isOpen"
|
||||||
class="**:stroke-white size-[26px]"
|
@click="isOpen = !isOpen"
|
||||||
/>
|
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-blue-500 size-[70px] flex justify-center items-center rounded-full"
|
||||||
</button>
|
>
|
||||||
|
<Icon
|
||||||
|
name="streamline:artificial-intelligence-spark"
|
||||||
|
class="**:stroke-white size-[26px]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<ChatBoxContainer :isOpen="isOpen" />
|
<ChatBoxContainer :isOpen="isOpen" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// types
|
// types
|
||||||
|
|
||||||
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
|
|
||||||
import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage";
|
import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage";
|
||||||
import { useToast } from "~/composables/global/useToast";
|
import { useToast } from "~/composables/global/useToast";
|
||||||
|
|
||||||
@@ -14,8 +13,7 @@ const { $queryClient: queryClient } = useNuxtApp();
|
|||||||
|
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: createMessage, isPending: isCreatingMessage } =
|
const { mutateAsync: createMessage, isPending: isCreatingMessage } = useCreateChatMessage(queryClient);
|
||||||
useCreateChatMessage(queryClient);
|
|
||||||
|
|
||||||
const chatInputEl = ref<HTMLInputElement | null>(null);
|
const chatInputEl = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
@@ -46,7 +44,10 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div id="poda" class="poda-rotate">
|
<div
|
||||||
|
id="poda"
|
||||||
|
class="poda-rotate"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="glow w-full"
|
class="glow w-full"
|
||||||
:class="isCreatingMessage ? '' : '!opacity-0'"
|
:class="isCreatingMessage ? '' : '!opacity-0'"
|
||||||
@@ -80,11 +81,7 @@ const sendMessage = async () => {
|
|||||||
<input
|
<input
|
||||||
ref="chatInputEl"
|
ref="chatInputEl"
|
||||||
:disabled="isCreatingMessage"
|
:disabled="isCreatingMessage"
|
||||||
:placeholder="
|
:placeholder="isCreatingMessage ? 'دارم فکر میکنم...' : 'سوال خود را بپرسید'"
|
||||||
isCreatingMessage
|
|
||||||
? 'دارم فکر میکنم...'
|
|
||||||
: 'سوال خود را بپرسید'
|
|
||||||
"
|
|
||||||
type="text"
|
type="text"
|
||||||
name="text"
|
name="text"
|
||||||
class="focus:outline-none h-full typo-p-sm w-full border-none"
|
class="focus:outline-none h-full typo-p-sm w-full border-none"
|
||||||
@@ -97,11 +94,11 @@ const sendMessage = async () => {
|
|||||||
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
|
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="fade-down">
|
<TransitionGroup name="fade-down">
|
||||||
<AiLoading
|
<Icon
|
||||||
v-if="isCreatingMessage"
|
v-if="isCreatingMessage"
|
||||||
circle
|
name="svg-spinners:wind-toy"
|
||||||
:size="75"
|
size="24"
|
||||||
class="mb-1"
|
class="text-black"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
v-else
|
v-else
|
||||||
@@ -188,14 +185,7 @@ const sendMessage = async () => {
|
|||||||
height: 600px;
|
height: 600px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
background-image: conic-gradient(
|
background-image: conic-gradient(transparent, #998fdc, transparent 10%, transparent 50%, #cf7bba, transparent 60%);
|
||||||
transparent,
|
|
||||||
#998fdc,
|
|
||||||
transparent 10%,
|
|
||||||
transparent 50%,
|
|
||||||
#cf7bba,
|
|
||||||
transparent 60%
|
|
||||||
);
|
|
||||||
transition: all 2s;
|
transition: all 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import useGetAccount from '~/composables/api/account/useGetAccount';
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,6 +23,7 @@ const emit = defineEmits(["textUpdate"]);
|
|||||||
// state
|
// state
|
||||||
|
|
||||||
const { $gsap: gsap } = useNuxtApp();
|
const { $gsap: gsap } = useNuxtApp();
|
||||||
|
const { data: account } = useGetAccount();
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
|
|
||||||
@@ -79,26 +82,32 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
v-if="reverse"
|
v-if="reverse"
|
||||||
src="/img/heymlz/footer-share.svg"
|
src="/img/heymlz/heymlz-full-body.jpg"
|
||||||
|
class="size-full object-cover absolute"
|
||||||
|
alt="profile"
|
||||||
|
/>
|
||||||
|
<NuxtImg
|
||||||
|
v-else
|
||||||
|
:src="account?.profile_photo ?? ''"
|
||||||
class="size-full object-cover absolute"
|
class="size-full object-cover absolute"
|
||||||
alt="profile"
|
alt="profile"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-150 px-4 py-3"
|
class="rounded-150 px-4 py-3 whitespace-pre-wrap overflow-hidden"
|
||||||
:class="
|
:class="
|
||||||
reverse
|
reverse
|
||||||
? 'bg-slate-100 text-slate-600'
|
? 'bg-slate-100 text-slate-600'
|
||||||
: 'bg-black text-white'
|
: 'bg-black text-white'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<p
|
<div
|
||||||
v-if="!loadingContent"
|
v-if="!loadingContent"
|
||||||
:id="`chat-message-content-${id}`"
|
:id="`chat-message-content-${id}`"
|
||||||
class="typo-p-sm font-normal whitespace-pre-wrap"
|
class="typo-p-sm font-normal whitespace-pre-wrap"
|
||||||
|
v-html="content"
|
||||||
>
|
>
|
||||||
{{ content }}
|
</div>
|
||||||
</p>
|
|
||||||
|
|
||||||
<Icon v-else name="svg-spinners:3-dots-bounce" size="20" />
|
<Icon v-else name="svg-spinners:3-dots-bounce" size="20" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user