Skip to content

Commit c8b2c3a

Browse files
authored
Merge pull request #235 from iceljc/features/refine-chat-window
add bubble chat
2 parents faec34e + 6b84a85 commit c8b2c3a

File tree

8 files changed

+205
-75
lines changed

8 files changed

+205
-75
lines changed

src/lib/common/BubbleChat.svelte

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script>
2+
/** @type {string} */
3+
export let text;
4+
5+
/** @type {() => void} */
6+
export let close = () => {};
7+
8+
/** @type {string} */
9+
export let containerClasses = "";
10+
11+
/** @type {string} */
12+
export let containerStyles = "";
13+
14+
/** @type {boolean} */
15+
export let disableDefaultStyles = false;
16+
17+
18+
/** @param {any} e */
19+
function handleClose(e) {
20+
e.preventDefault();
21+
close?.();
22+
}
23+
</script>
24+
25+
<div
26+
class="{disableDefaultStyles ? '' : 'chat-bubble-container'} {containerClasses}"
27+
style={`${containerStyles}`}
28+
>
29+
<div class="chat-bubble">
30+
<!-- svelte-ignore a11y-click-events-have-key-events -->
31+
<!-- svelte-ignore a11y-no-static-element-interactions -->
32+
<div
33+
class="bubble-delete clickable"
34+
on:click={e => handleClose(e)}
35+
>
36+
<i class="mdi mdi-close" />
37+
</div>
38+
<div class="bubble-text">
39+
{text}
40+
</div>
41+
</div>
42+
</div>

src/lib/common/LiveChatEntry.svelte

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import { onMount } from 'svelte';
44
import { PUBLIC_LIVECHAT_HOST, PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public';
55
import { getSettingDetail } from '$lib/services/setting-service';
6-
import { chatBotStore } from '$lib/helpers/store';
76
import { CHAT_FRAME_ID } from '$lib/helpers/constants';
87
import { ChatAction } from '$lib/helpers/enums';
8+
import BubbleChat from './BubbleChat.svelte';
99
1010
let chatUrl = PUBLIC_LIVECHAT_HOST;
11+
let showChatBox = false;
12+
let showBubbleMsg = false;
13+
let receivedMsg = '';
1114
1215
onMount(async () => {
1316
const agentSettings = await getSettingDetail("Agent");
@@ -17,33 +20,61 @@
1720
// Handle event from iframe
1821
window.onmessage = async function(e) {
1922
if (e.data.action == ChatAction.Close) {
20-
chatBotStore.set({ showChatBox: false });
23+
showChatBox = false;
2124
} else if (e.data.action == ChatAction.Open) {
22-
chatBotStore.set({ showChatBox: true });
25+
// showChatBox = true;
26+
} else if (e.data.action == ChatAction.ReceiveMsg && !showChatBox) {
27+
receivedMsg = e.data?.data?.rich_content?.message?.text || e.data?.data?.text || '';
28+
showBubbleMsg = true;
29+
wave();
2330
}
2431
};
2532
2633
function openChatBox() {
27-
chatBotStore.set({ showChatBox: true });
34+
showChatBox = true;
35+
receivedMsg = '';
36+
showBubbleMsg = false;
37+
}
38+
39+
function closeBubbleMsg() {
40+
receivedMsg = '';
41+
showBubbleMsg = false;
42+
}
43+
44+
function wave() {
45+
const elem = document.getElementById('chatbot-icon');
46+
if (elem) {
47+
elem.classList.add('waving');
48+
setTimeout(() => {
49+
elem.classList.remove('waving');
50+
}, 800);
51+
}
2852
}
2953
</script>
3054
3155
<div class="chatbot-container fixed-bottom float-bottom-right">
56+
{#if showBubbleMsg}
57+
<div transition:fade={{ delay: 50, duration: 200 }}>
58+
<BubbleChat text={receivedMsg} close={() => closeBubbleMsg()} />
59+
</div>
60+
{/if}
61+
3262
<iframe
3363
src={chatUrl}
34-
width={`${$chatBotStore.showChatBox ? '380px' : '0px'}`}
35-
height={`${$chatBotStore.showChatBox ? '650px' : '0px'}`}
36-
class={`border border-2 rounded-3 m-3 float-end ${$chatBotStore.showChatBox ? 'chat-iframe' : ''}`}
64+
width={'380px'}
65+
height={'650px'}
66+
class={`border border-2 rounded-3 m-3 float-end ${showChatBox ? 'chat-iframe' : 'hide'}`}
3767
title="live chat"
3868
id={CHAT_FRAME_ID}
3969
/>
4070
41-
{#if !$chatBotStore.showChatBox}
71+
{#if !showChatBox}
4272
<div
73+
id="chatbot-icon"
4374
class="chatbot-icon mb-3 float-end wave-effect"
4475
transition:fade={{ delay: 50, duration: 200 }}
4576
>
46-
<button class="btn btn-transparent" on:click={() => openChatBox()}>
77+
<button class="btn btn-transparent chat-icon-btn" on:click={() => openChatBox()}>
4778
<img alt="live chat" class="avatar-md rounded-circle" src={PUBLIC_LIVECHAT_ENTRY_ICON} />
4879
</button>
4980
</div>
@@ -58,6 +89,13 @@
5889
perspective: 1000px;
5990
}
6091
92+
.waving {
93+
animation: wave 0.82s cubic-bezier(.36,.07,.19,.97) both;
94+
transform: translate3d(0, 0, 0);
95+
backface-visibility: hidden;
96+
perspective: 1000px;
97+
}
98+
6199
@keyframes wave {
62100
10%, 90% {
63101
transform: translate3d(-1px, 0, 0);
@@ -91,4 +129,8 @@
91129
display: flex;
92130
justify-content: flex-end;
93131
}
132+
133+
.chat-icon-btn {
134+
padding-top: 0px;
135+
}
94136
</style>

src/lib/helpers/enums.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ const chatAction = {
105105
Close: 'close',
106106
Logout: 'logout',
107107
Chat: 'chat',
108-
NewChat: 'new-chat'
108+
NewChat: 'new-chat',
109+
ReceiveMsg: 'receive-msg'
109110
};
110111
export const ChatAction = Object.freeze(chatAction);

src/lib/helpers/store.js

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,33 +35,35 @@ userStore.subscribe(value => {
3535
});
3636

3737

38-
/** @type {Writable<import('$conversationTypes').ConversationModel>}*/
39-
export const conversationStore = writable({});
40-
41-
/**
42-
* @returns {Writable<import('$conversationTypes').ConversationModel>}
43-
*/
44-
export function getConversationStore() {
45-
if (browser) {
46-
// Access localStorage only if in the browser context
47-
const json = localStorage.getItem(conversationKey);
48-
if (json)
49-
return JSON.parse(json);
50-
else
51-
return conversationStore;
52-
} else {
53-
// Return a default value for SSR
54-
return conversationStore;
55-
}
38+
const createConversationStore = () => {
39+
const { subscribe } = writable({});
40+
return {
41+
clear: (/** @type {string | null} */ convId = null) => {
42+
if (!convId) {
43+
localStorage.removeItem(conversationKey);
44+
return;
45+
}
46+
47+
const json = localStorage.getItem(conversationKey);
48+
if (json) {
49+
const conv = JSON.parse(json);
50+
if (conv.id === convId) {
51+
localStorage.removeItem(conversationKey);
52+
}
53+
}
54+
},
55+
get: () => {
56+
const json = localStorage.getItem(conversationKey);
57+
return json ? JSON.parse(json) : {};
58+
},
59+
put: (value) => {
60+
localStorage.setItem(conversationKey, JSON.stringify(value));
61+
},
62+
subscribe
63+
};
5664
};
5765

58-
// @ts-ignore
59-
conversationStore.subscribe(value => {
60-
if (browser && value.id) {
61-
localStorage.setItem(conversationKey, JSON.stringify(value));
62-
}
63-
});
64-
66+
export const conversationStore = createConversationStore();
6567

6668

6769
const createLoaderStore = () => {
@@ -182,19 +184,6 @@ const createKnowledgeBaseDocumentStore = () => {
182184
export const knowledgeBaseDocumentStore = createKnowledgeBaseDocumentStore();
183185

184186

185-
const createChatBotStore = () => {
186-
const { subscribe, set, update } = writable({ showChatBox: false });
187-
188-
return {
189-
set,
190-
update,
191-
subscribe
192-
}
193-
};
194-
195-
export const chatBotStore = createChatBotStore();
196-
197-
198187
export function resetLocalStorage(resetUser = false) {
199188
conversationUserStateStore.resetAll();
200189
conversationSearchOptionStore.reset();

src/lib/scss/custom/components/_chat.scss

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
$bubble-chat-theme-color: rgba($primary, 80%);
2+
13
.chat-util-common {
24
display: block;
35
position: absolute;
@@ -41,4 +43,48 @@
4143
.chat-util-item {
4244
text-align: center;
4345
font-size: 30px;
44-
}
46+
}
47+
48+
49+
50+
.chat-bubble-container {
51+
display: flex;
52+
flex-direction: column;
53+
gap: 0px;
54+
55+
.chat-bubble {
56+
margin: 5px 30px 0px 5px;
57+
display: inline-block;
58+
position: relative;
59+
min-width: 200px;
60+
max-width: 300px;
61+
background-color: $bubble-chat-theme-color;
62+
border-radius: .4em;
63+
color: white;
64+
65+
&:after {
66+
content: ' ';
67+
position: absolute;
68+
top: 100%;
69+
right: 50px;
70+
width: 25px;
71+
height: 20px;
72+
clip-path: polygon(0 0, 100% 0, 100% 100%);
73+
background-color: $bubble-chat-theme-color;
74+
}
75+
76+
.bubble-text {
77+
margin: 15px;
78+
height: fit-content;
79+
max-height: 100px;
80+
overflow-y: auto;
81+
scrollbar-width: none;
82+
}
83+
84+
.bubble-delete {
85+
position: absolute;
86+
right: 5px;
87+
top: 3px;
88+
}
89+
}
90+
}

src/routes/chat/[agentId]/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { newConversation } from '$lib/services/conversation-service.js';
88
import { getToken, setToken } from '$lib/services/auth-service.js'
99
import { getUserStore } from '$lib/helpers/store.js';
10-
import { conversationStore, getConversationStore } from '$lib/helpers/store.js';
10+
import { conversationStore } from '$lib/helpers/store.js';
1111
import { LERNER_ID, TRAINING_MODE } from '$lib/helpers/constants';
1212
1313
const params = $page.params;
@@ -32,11 +32,11 @@
3232
});
3333
}
3434
35-
conversation = getConversationStore();
35+
conversation = conversationStore.get();
3636
if (!conversation.id || agentId != conversation.agent_id) {
3737
// new conversation
3838
conversation = await newConversation(agentId);
39-
conversationStore.set(conversation);
39+
conversationStore.put(conversation);
4040
}
4141
4242
conversationId = conversation.id;

0 commit comments

Comments
 (0)