diff --git a/src/lib/common/LiveChatEntry.svelte b/src/lib/common/LiveChatEntry.svelte index fc952659..755e41be 100644 --- a/src/lib/common/LiveChatEntry.svelte +++ b/src/lib/common/LiveChatEntry.svelte @@ -1,85 +1,86 @@ <script> - import { fade } from 'svelte/transition'; - import { onMount } from 'svelte'; - import { PUBLIC_LIVECHAT_HOST, PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public'; - import { getSettingDetail } from '$lib/services/setting-service'; + import { fade } from 'svelte/transition'; + import { onMount } from 'svelte'; + import { PUBLIC_LIVECHAT_HOST, PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public'; + import { getSettingDetail } from '$lib/services/setting-service'; - let showChatIcon = false; - let showChatBox = false; - let chatUrl = PUBLIC_LIVECHAT_HOST; + let showChatIcon = false; + let showChatBox = false; + let chatUrl = PUBLIC_LIVECHAT_HOST; - onMount(async () => { - const agentSettings = await getSettingDetail("Agent"); - chatUrl = `${PUBLIC_LIVECHAT_HOST}chat/${agentSettings.hostAgentId}?isFrame=true`; - showChatIcon = true; - }); + onMount(async () => { + const agentSettings = await getSettingDetail("Agent"); + chatUrl = `${PUBLIC_LIVECHAT_HOST}chat/${agentSettings.hostAgentId}?isFrame=true`; + showChatIcon = true; + }); - // Handle event from iframe - window.onmessage = async function(e) { - if (e.data.action == 'close') { - showChatIcon = true; - showChatBox = false; - } - }; + // Handle event from iframe + window.onmessage = async function(e) { + if (e.data.action == 'close') { + showChatIcon = true; + showChatBox = false; + } + }; - function handleChatBox() { - showChatIcon = false; - showChatBox = true; - } + function handleChatBox() { + showChatIcon = false; + showChatBox = true; + } </script> <div class="fixed-bottom float-bottom-right"> - {#if showChatBox} - <div transition:fade={{ delay: 250, duration: 300 }}> - <iframe - src={chatUrl} - width="380px" - height="650px" - class="border border-2 rounded-3 m-3 float-end chat-iframe" - title="live chat" - > - </iframe> - </div> - {/if} + {#if showChatBox} + <div transition:fade={{ delay: 250, duration: 300 }}> + <iframe + src={chatUrl} + width="380px" + height="650px" + class="border border-2 rounded-3 m-3 float-end chat-iframe" + title="live chat" + id="chat-frame" + > + </iframe> + </div> + {/if} - {#if showChatIcon} - <div class="mb-3 float-end wave-effect" transition:fade={{ delay: 100, duration: 500 }}> - <button class="btn btn-transparent" on:click={handleChatBox}> - <img alt="live chat" class="avatar-md rounded-circle" src={PUBLIC_LIVECHAT_ENTRY_ICON} /> - </button> - </div> - {/if} + {#if showChatIcon} + <div class="mb-3 float-end wave-effect" transition:fade={{ delay: 100, duration: 500 }}> + <button class="btn btn-transparent" on:click={() => handleChatBox()}> + <img alt="live chat" class="avatar-md rounded-circle" src={PUBLIC_LIVECHAT_ENTRY_ICON} /> + </button> + </div> + {/if} </div> <style> - .wave-effect:hover { - animation: wave 0.82s cubic-bezier(.36,.07,.19,.97) both; - transform: translate3d(0, 0, 0); - backface-visibility: hidden; - perspective: 1000px; - } - - @keyframes wave { - 10%, 90% { - transform: translate3d(-1px, 0, 0); - } - - 20%, 80% { - transform: translate3d(2px, 0, 0); + .wave-effect:hover { + animation: wave 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; } - 30%, 50%, 70% { - transform: translate3d(-4px, 0, 0); - } + @keyframes wave { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } - 40%, 60% { - transform: translate3d(4px, 0, 0); + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } } - } - .float-bottom-right { - width: fit-content; - margin-right: 0px; - margin-left: auto; - } + .float-bottom-right { + width: fit-content; + margin-right: 0px; + margin-left: auto; + } </style> \ No newline at end of file diff --git a/src/lib/common/ProfileDropdown.svelte b/src/lib/common/ProfileDropdown.svelte index 6d51f2fa..943250a9 100644 --- a/src/lib/common/ProfileDropdown.svelte +++ b/src/lib/common/ProfileDropdown.svelte @@ -15,6 +15,13 @@ if (browser){ resetLocalStorage(true); } + + const chatFrame = document.getElementById('chat-frame'); + if (chatFrame) { + // @ts-ignore + chatFrame.contentWindow.postMessage({ action: "logout" }, "*"); + } + goto('login'); }; @@ -57,7 +64,7 @@ role="button" tabindex="0" on:keydown={() => {}} - on:click={logout} + on:click={() => logout()} > <i class="bx bx-power-off font-size-16 align-middle me-1 text-danger" /> <span>{$_('Logout')}</span> </div> diff --git a/src/lib/helpers/enums.js b/src/lib/helpers/enums.js index 35736909..ed622d67 100644 --- a/src/lib/helpers/enums.js +++ b/src/lib/helpers/enums.js @@ -28,7 +28,8 @@ export const RichType = Object.freeze(richType); const elementType = { Text: "text", Video: "video", - File: "file" + File: "file", + Web: "web_url" }; export const ElementType = Object.freeze(elementType); diff --git a/src/lib/helpers/store.js b/src/lib/helpers/store.js index 91c6a639..5aa79dec 100644 --- a/src/lib/helpers/store.js +++ b/src/lib/helpers/store.js @@ -2,6 +2,7 @@ import { writable } from 'svelte/store'; import { browser } from '$app/environment'; +const userKey = "user"; const conversationKey = "conversation"; const conversationUserStatesKey = "conversation_user_states"; const conversationSearchOptionKey = "conversation_search_option"; @@ -16,7 +17,7 @@ export const userStore = writable({ id: "", full_name: "", expires: 0, token: nu export function getUserStore() { if (browser) { // Access localStorage only if in the browser context - let json = localStorage.getItem('user'); + let json = localStorage.getItem(userKey); if (json) return JSON.parse(json); else @@ -29,7 +30,7 @@ export function getUserStore() { userStore.subscribe(value => { if (browser && value.token) { - localStorage.setItem('user', JSON.stringify(value)); + localStorage.setItem(userKey, JSON.stringify(value)); } }); diff --git a/src/lib/scss/custom/pages/_chat.scss b/src/lib/scss/custom/pages/_chat.scss index bf45728f..e545e573 100644 --- a/src/lib/scss/custom/pages/_chat.scss +++ b/src/lib/scss/custom/pages/_chat.scss @@ -749,6 +749,10 @@ margin-left: 0px !important; border-radius: 10px; } + + .btn-link { + background-color: unset !important; + } } } } diff --git a/src/routes/(home)/+page.svelte b/src/routes/(home)/+page.svelte index 25f1fd8d..bf8753e5 100644 --- a/src/routes/(home)/+page.svelte +++ b/src/routes/(home)/+page.svelte @@ -41,17 +41,17 @@ <Row class="justify-content-center mt-5"> <Col sm="8"> {#if showHomeImage} - <div class="maintenance-img" transition:fade={{ delay: 300, duration: 500 }}> - <img src={PUBLIC_HOME_IMAGE} alt="" style="max-width: 25vw;" /> - </div> + <div class="maintenance-img" transition:fade={{ delay: 300, duration: 500 }}> + <img src={PUBLIC_HOME_IMAGE} alt="" style="max-width: 25vw;" /> + </div> {/if} </Col> </Row> {#if showHomeSlogan} - <h4 class="mt-5" transition:fade={{ delay: 500, duration: 500 }}>Let's <a href="login" class="btn btn-primary">get started</a> with {PUBLIC_BRAND_NAME}</h4> - <p class="text-muted" transition:fade={{ delay: 800, duration: 500 }}> - {PUBLIC_HOME_SLOGAN} - </p> + <h4 class="mt-5" transition:fade={{ delay: 500, duration: 500 }}>Let's <a href="login" class="btn btn-primary">get started</a> with {PUBLIC_BRAND_NAME}</h4> + <p class="text-muted" transition:fade={{ delay: 800, duration: 500 }}> + {PUBLIC_HOME_SLOGAN} + </p> {/if} </div> </Col> diff --git a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte index d4bcb4b1..951609da 100644 --- a/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/chat-box.svelte @@ -18,7 +18,8 @@ conversationStore, conversationUserStateStore, conversationUserMessageStore, - conversationUserAttachmentStore + conversationUserAttachmentStore, + resetLocalStorage } from '$lib/helpers/store.js'; import { sendMessageToHub, @@ -166,6 +167,12 @@ }); onMount(async () => { + window.addEventListener('message', e => { + if (e.data.action === 'logout') { + resetLocalStorage(true); + } + }); + autoScrollLog = true; dialogs = await GetDialogs(params.conversationId); conversationUser = await getConversationUser(params.conversationId); diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte index 02e18900..f81f3808 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-complex-options.svelte @@ -2,6 +2,7 @@ import { getContext, onMount } from "svelte"; import { fade } from 'svelte/transition'; import { Card, CardBody } from "@sveltestrap/sveltestrap"; + import { ElementType } from "$lib/helpers/enums"; /** @type {boolean} */ export let disabled = false; @@ -35,6 +36,8 @@ return { title: x.title, payload: x.payload, + type: x.type, + url: x.url, is_primary: x.is_primary, is_secondary: x.is_secondary, }; @@ -54,6 +57,8 @@ return { title: x.title, payload: x.payload, + type: x.type, + url: x.url, is_primary: x.is_primary, is_secondary: x.is_secondary, }; @@ -75,7 +80,7 @@ * @param {string} payload */ function innerConfirm(title, payload) { - onConfirm && onConfirm(title, payload); + onConfirm?.(title, payload); reset(); } @@ -106,13 +111,23 @@ {#if card.options?.length > 0} <div class="card-option-group"> {#each card.options as option, i (i)} - <button - class={`btn btn-sm m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} - disabled={disabled} - on:click={(e) => handleClickOption(e, option)} - > - {option.title} - </button> + {#if option.type === ElementType.Web && option.url} + <button + class={`btn btn-sm btn-link m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} + disabled={disabled} + on:click={() => window.open(option.url)} + > + {option.title} + </button> + {:else} + <button + class={`btn btn-sm m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} + disabled={disabled} + on:click={(e) => handleClickOption(e, option)} + > + {option.title} + </button> + {/if} {/each} </div> {/if} diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte index ecb0bef9..c65a7a3b 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rc-plain-options.svelte @@ -57,6 +57,8 @@ return { title: op.title, payload: op.payload, + type: op.type, + url: op.url, is_primary: op.is_primary, is_secondary: op.is_secondary, isClicked: false @@ -144,15 +146,26 @@ {#if plainOptions || fileOption} <div class="plain-option-container center-option"> {#each plainOptions as option, index} - <button - class={`btn btn-sm m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} - class:active={!!option.isClicked} - disabled={disabled} - in:fade={{ duration: duration }} - on:click={(e) => handleClickOption(e, option, index)} - > - {option.title} - </button> + {#if option.type === ElementType.Web && option.url} + <button + class={`btn btn-sm btn-link m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} + disabled={disabled} + in:fade={{ duration: duration }} + on:click={() => window.open(option.url)} + > + {option.title} + </button> + {:else} + <button + class={`btn btn-sm m-1 ${option.is_secondary ? 'btn-outline-secondary': 'btn-outline-primary'}`} + class:active={!!option.isClicked} + disabled={disabled} + in:fade={{ duration: duration }} + on:click={(e) => handleClickOption(e, option, index)} + > + {option.title} + </button> + {/if} {/each} {#if plainOptions && isMultiSelect} <button diff --git a/src/routes/chat/[agentId]/[conversationId]/rich-content/rich-content.svelte b/src/routes/chat/[agentId]/[conversationId]/rich-content/rich-content.svelte index 3786ecf6..5db267eb 100644 --- a/src/routes/chat/[agentId]/[conversationId]/rich-content/rich-content.svelte +++ b/src/routes/chat/[agentId]/[conversationId]/rich-content/rich-content.svelte @@ -44,18 +44,18 @@ * @param {string} payload */ function handleConfirm(title, payload) { - onConfirm && onConfirm(title, payload); + onConfirm?.(title, payload); } </script> {#if message?.rich_content?.editor === EditorType.File} - <ChatAttachmentOptions options={options} disabled={disabled} onConfirm={handleConfirm} /> + <ChatAttachmentOptions options={options} disabled={disabled} onConfirm={(title, payload) => handleConfirm(title, payload)} /> {/if} {#if message?.rich_content?.editor !== EditorType.File} {#if !isComplexElement} - <RcPlainOptions options={options} isMultiSelect={isMultiSelect} disabled={disabled} onConfirm={handleConfirm} /> + <RcPlainOptions options={options} isMultiSelect={isMultiSelect} disabled={disabled} onConfirm={(title, payload) => handleConfirm(title, payload)} /> {:else} - <RcComplexOptions options={options} disabled={disabled} onConfirm={handleConfirm} /> + <RcComplexOptions options={options} disabled={disabled} onConfirm={(title, payload) => handleConfirm(title, payload)} /> {/if} {/if} \ No newline at end of file