Fix chat box and make it responsive

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