Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/lib/helpers/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ const senderAction = {
MarkSeen: 3
}

export const SenderAction = Object.freeze(senderAction);
export const SenderAction = Object.freeze(senderAction);

const richType = {
Text: 'text',
QuickReply: 'quick_reply',
Button: 'button_template',
MultiSelect: 'multi-select_template'
}

export const RichType = Object.freeze(richType);
5 changes: 5 additions & 0 deletions src/lib/scss/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,8 @@ File: Main Css File
.swal2-container .swal2-confirm {
background-color: var(--bs-primary);
}

button:focus {
outline: none !important;
box-shadow: none !important;
}
17 changes: 17 additions & 0 deletions src/lib/scss/custom/pages/_chat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,23 @@
.msg-container {
max-width: 80%;
flex: 0 0 fit-content;

.button-group {
display: flex;
flex-wrap: wrap;

.btn:not([name="confirm"]) {
&:hover {
background-color: #fff;
color: var(--bs-primary);
}
}

.btn.active:hover {
background-color: var(--bs-primary);
color: #fff;
}
}
}

.ctext-wrap {
Expand Down
84 changes: 49 additions & 35 deletions src/routes/chat/[agentId]/[conversationId]/chat-box.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,32 @@
import { page } from '$app/stores';
import { onMount, tick } from 'svelte';
import Link from 'svelte-link';
import { PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public';
import { signalr } from '$lib/services/signalr-service.js';
import { webSpeech } from '$lib/services/web-speech.js';
import { sendMessageToHub, GetDialogs, deleteConversationMessage } from '$lib/services/conversation-service.js';
import { newConversation } from '$lib/services/conversation-service';
import { conversationStore, conversationUserStateStore } from '$lib/helpers/store.js';
import DialogModal from '$lib/common/DialogModal.svelte';
import HeadTitle from '$lib/common/HeadTitle.svelte';
import LoadingDots from '$lib/common/LoadingDots.svelte';
import { utcToLocal } from '$lib/helpers/datetime';
import { replaceNewLine } from '$lib/helpers/http';
import { SenderAction, UserRole, RichType } from '$lib/helpers/enums';
import RcText from './messageComponents/rc-text.svelte';
import RcQuickReply from './messageComponents/rc-quick-reply.svelte';
import { PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public';
import RcMarkdown from './messageComponents/rc-markdown.svelte';
import RcButton from './messageComponents/rc-button.svelte';
import RcMultiSelect from './messageComponents/rc-multi-select.svelte';
import ContentLog from './contentLogs/content-log.svelte';
import { replaceNewLine } from '$lib/helpers/http';
import _ from "lodash";
import Swal from 'sweetalert2/dist/sweetalert2.js';
import "sweetalert2/src/sweetalert2.scss";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import StateLog from './stateLogs/state-log.svelte';
import StateModal from './addStateModal/state-modal.svelte';
import DialogModal from '$lib/common/DialogModal.svelte';
import { SenderAction, UserRole } from '$lib/helpers/enums';
import moment from 'moment';
import HeadTitle from '$lib/common/HeadTitle.svelte';
import RcMarkdown from './messageComponents/rc-markdown.svelte';
import LoadingDots from '$lib/common/LoadingDots.svelte';
import ChatImage from './chatImage/chat-image.svelte';
import Swal from 'sweetalert2/dist/sweetalert2.js';
import "sweetalert2/src/sweetalert2.scss";
import moment from 'moment';

const options = {
scrollbars: {
Expand Down Expand Up @@ -66,6 +68,7 @@
// @ts-ignore
let scrollbar;
let microphoneIcon = "microphone-off";
let lastBotMsgId = '';

/** @type {import('$types').ChatResponseModel[]} */
let dialogs = [];
Expand Down Expand Up @@ -121,9 +124,16 @@
isLoadContentLog = !isLite;
}

/** @param {import('$types').ChatResponseModel[]} dialogs */
function findLastBotMessage(dialogs) {
const lastBotMsg = dialogs.findLast(x => x.sender?.role === UserRole.Assistant);
return lastBotMsg?.message_id || '';
}

async function refresh() {
// trigger UI render
dialogs = dialogs?.map(item => { return { ...item }; }) || [];
lastBotMsgId = findLastBotMessage(dialogs);
groupedDialogs = groupDialogs(dialogs);
await tick();

Expand Down Expand Up @@ -166,13 +176,6 @@
/** @param {import('$types').ChatResponseModel} message */
function onMessageReceivedFromAssistant(message) {
// webSpeech.utter(message.text);
// clean rich content elements
dialogs.forEach(dialog => {
if (dialog.rich_content && dialog.rich_content.message.rich_type == "quick_reply") {
dialog.rich_content.message.quick_replies = [];
}
});

dialogs.push(message);
refresh();
}
Expand Down Expand Up @@ -290,16 +293,13 @@

/**
* @param {string} payload
* @param {import('$types').QuickReplyMessage} message
*/
function clickQuickReply(payload, message) {
function confirmSelectedOption(payload) {
isSendingMsg = true;
sendMessageToHub(params.agentId, params.conversationId, payload).then(() => {
isSendingMsg = false;
message.quick_replies = [];
}).catch(() => {
isSendingMsg = false;
message.quick_replies = [];
});
}

Expand Down Expand Up @@ -576,20 +576,34 @@
{/if}
</div>
<div class="msg-container">
<div class="ctext-wrap">
<div class="flex-shrink-0 align-self-center">
{#if message.rich_content && message.rich_content.message.rich_type == 'text'}
<RcText message={message.rich_content.message} />
{:else if message.rich_content && message.rich_content.message.rich_type == 'quick_reply'}
<RcQuickReply
message={message.rich_content.message}
onClickQuickReply={clickQuickReply}
/>
{:else}
<span>{message.text}</span>
{/if}
</div>
</div>
{#if message.rich_content}
{#if message.rich_content.message?.rich_type === RichType.Text}
<RcText message={message.rich_content.message} />
{:else if message.rich_content.message?.rich_type === RichType.QuickReply}
<RcQuickReply
message={message.rich_content?.message}
displayOptions={message.message_id === lastBotMsgId}
onConfirm={confirmSelectedOption}
/>
{:else if message.rich_content.message?.rich_type === RichType.Button}
<RcButton
message={message.rich_content.message}
displayOptions={message.message_id === lastBotMsgId}
onConfirm={confirmSelectedOption}
/>
{:else if message.rich_content.message?.rich_type === RichType.MultiSelect}
<RcMultiSelect
message={message.rich_content.message}
displayOptions={message.message_id === lastBotMsgId}
onConfirm={confirmSelectedOption}
/>
{:else}
<RcText message={message} />
{/if}
{:else}
<RcText message={message} />
{/if}

<ChatImage message={message} />
</div>
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script>
import { replaceNewLine } from "$lib/helpers/http";
import RcOptions from "./rc-options.svelte";

/** @type {any} */
export let message;

/** @type {boolean} */
export let displayOptions = false;

/** @type {(args0: string) => any} */
export let onConfirm;

/**
* @param {string} payload
*/
function handleConfirm(payload) {
onConfirm && onConfirm(payload);
}
</script>

<div class="ctext-wrap">
<div class="flex-shrink-0 align-self-center">
<span>{@html replaceNewLine(message?.text || '')}</span>
</div>
</div>

{#if displayOptions && message?.buttons?.length > 0}
<RcOptions options={message.buttons} onConfirm={handleConfirm} />
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
export let message = "The SQL statement to create a new service category named 'DSC Equipment' is: <br /> ```sql INSERT INTO data_ServiceCategory (Name) VALUES ('DSC Equipment');``` <br /> I have executed the query to create the new service category.";
</script>

<span>{@html marked(replaceNewLine(message || ''))}</span>
<div class="ctext-wrap">
<div class="flex-shrink-0 align-self-center">
<span>{@html marked(replaceNewLine(message || ''))}</span>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script>
import { replaceNewLine } from "$lib/helpers/http";
import RcOptions from "./rc-options.svelte";

/** @type {any} */
export let message;

/** @type {boolean} */
export let displayOptions = false;

/** @type {(args0: string) => any} */
export let onConfirm;

/**
* @param {string} payload
*/
function handleConfirm(payload) {
onConfirm && onConfirm(payload);
}
</script>

<div class="ctext-wrap">
<div class="flex-shrink-0 align-self-center">
<span>{@html replaceNewLine(message?.text || '')}</span>
</div>
</div>

{#if displayOptions && message?.options?.length > 0}
<RcOptions options={message.options} isMultiSelect onConfirm={handleConfirm} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script>
import { onMount } from "svelte";

/** @type {boolean} */
export let isMultiSelect = false;

/** @type {any[]} */
export let options = [];

/** @type {(args0: string) => any} */
export let onConfirm;

/** @type {string} */
export let confirmBtnText = 'Continue';

const separator = '|';

/** @type {string[]} */
let answers = [];
/** @type {any[]} */
let localOptions = [];

onMount(() => {
answers = [];
localOptions = options?.map(op => {
return {
...op,
isClicked: false
}
}) || [];
});

/**
* @param {any} e
* @param {string} payload
* @param {number} index
*/
function handleClickOption(e, payload, index) {
e.preventDefault();

if (!isMultiSelect) {
innerConfirm(payload);
} else {
localOptions = localOptions.map((op, idx) => {
if (idx === index) {
op.isClicked = !op.isClicked;
if (op.isClicked) {
answers = [...answers, op.payload];
} else {
answers = answers.filter(a => a != op.payload);
}
}
return op;
});
}
}

/**
* @param {any} e
*/
function handleConfirm(e) {
e.preventDefault();
const payload = answers.join(separator);
innerConfirm(payload);
}

/**
* @param {string} payload
*/
function innerConfirm(payload) {
onConfirm && onConfirm(payload);
reset();
}

function reset() {
localOptions = [];
answers = [];
}

</script>

<div class="button-group">
{#each localOptions as option, index}
<button class="btn btn-outline-primary btn-rounded btn-sm m-1" class:active={!!option.isClicked} on:click={(e) => handleClickOption(e, option.payload, index)}>{option.title}</button>
{/each}
{#if isMultiSelect}
<button class="btn btn-outline-success btn-rounded btn-sm m-1" name="confirm" disabled={localOptions.every(x => !!!x.isClicked)} on:click={(e) => handleConfirm(e)}>{confirmBtnText}</button>
{/if}
</div>
Loading