From b90459305e44b7c1a8fbd8d34fe2ef446e85ad8f Mon Sep 17 00:00:00 2001 From: PedroMiolaSilva <pedro.silva@azion.com> Date: Wed, 19 Feb 2025 09:22:47 -0300 Subject: [PATCH 1/4] feat: adding basic auth + clerk --- .../vue/vue3-ai-chatbot-widget/src/App.vue | 6 +- .../src/assets/auth-modal.css | 64 ++++++++++ .../src/components/BasicAuthWindow.vue | 65 ++++++++++ .../src/components/layout-chat/index.vue | 50 +++++++- .../src/composables/useAzionCopilot.js | 8 +- .../src/core/azion-copilot.js | 31 ++++- .../src/core/constants.js | 3 +- .../vue/vue3-ai-chatbot-widget/src/main.js | 46 +++++-- .../src/services/auth.js | 116 ++++++++++++++++++ 9 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 templates/vue/vue3-ai-chatbot-widget/src/assets/auth-modal.css create mode 100644 templates/vue/vue3-ai-chatbot-widget/src/components/BasicAuthWindow.vue create mode 100644 templates/vue/vue3-ai-chatbot-widget/src/services/auth.js diff --git a/templates/vue/vue3-ai-chatbot-widget/src/App.vue b/templates/vue/vue3-ai-chatbot-widget/src/App.vue index 7d96d2ee..2028c4a5 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/App.vue +++ b/templates/vue/vue3-ai-chatbot-widget/src/App.vue @@ -15,7 +15,8 @@ isOpenByDefault: Boolean, isMaximizedByDefault: Boolean, previewText: String, - footerDisclaimer: String + footerDisclaimer: String, + authMode: String }) const chatWidget = reactive({ @@ -27,7 +28,8 @@ isOpenChat: props.isOpenByDefault, isMaximizedChat: props.isMaximizedByDefault, previewText: props.previewText, - footerDisclaimer: props.footerDisclaimer + footerDisclaimer: props.footerDisclaimer, + authMode: props.authMode }) provide('chatWidget', chatWidget) diff --git a/templates/vue/vue3-ai-chatbot-widget/src/assets/auth-modal.css b/templates/vue/vue3-ai-chatbot-widget/src/assets/auth-modal.css new file mode 100644 index 00000000..8706473d --- /dev/null +++ b/templates/vue/vue3-ai-chatbot-widget/src/assets/auth-modal.css @@ -0,0 +1,64 @@ +.auth-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.auth-modal { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +input { + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +button { + padding: 0.5rem 1rem; + background: #f76707; /* Orange color */ + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background: #e85d04; /* Darker orange on hover */ +} + +button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.error-message { + color: #dc3545; + font-size: 0.875rem; + padding: 0.5rem; + background: #fde8e8; + border-radius: 4px; +} + +.loading-message { + color: #666; + font-size: 0.875rem; + text-align: center; +} \ No newline at end of file diff --git a/templates/vue/vue3-ai-chatbot-widget/src/components/BasicAuthWindow.vue b/templates/vue/vue3-ai-chatbot-widget/src/components/BasicAuthWindow.vue new file mode 100644 index 00000000..9334ae78 --- /dev/null +++ b/templates/vue/vue3-ai-chatbot-widget/src/components/BasicAuthWindow.vue @@ -0,0 +1,65 @@ +<template> + <div class="auth-modal-overlay"> + <div class="auth-modal"> + <h2>Authentication Required</h2> + <form class="auth-form" @submit.prevent="handleSubmit"> + <div v-if="error" class="error-message"> + {{ error }} + </div> + <div v-if="loading" class="loading-message"> + Loading... + </div> + <input + type="password" + v-model="password" + placeholder="Enter password" + :disabled="loading" + autofocus + /> + <button + type="submit" + :disabled="loading" + > + {{ loading ? 'Signing in...' : 'Sign In' }} + </button> + </form> + </div> + </div> +</template> + +<script setup> +import { ref } from 'vue' + +const props = defineProps({ + onSubmit: { + type: Function, + required: true + } +}) + +const password = ref('') +const loading = ref(false) +const error = ref('') + +const handleSubmit = async () => { + if (!password.value) { + error.value = 'Password is required' + return + } + + loading.value = true + error.value = '' + + try { + await props.onSubmit(password.value) + } catch (err) { + error.value = err.message || 'Invalid password' + } finally { + loading.value = false + } +} +</script> + +<style scoped> +@import '../assets/auth-modal.css'; +</style> \ No newline at end of file diff --git a/templates/vue/vue3-ai-chatbot-widget/src/components/layout-chat/index.vue b/templates/vue/vue3-ai-chatbot-widget/src/components/layout-chat/index.vue index 4a883db8..eac5ff9f 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/components/layout-chat/index.vue +++ b/templates/vue/vue3-ai-chatbot-widget/src/components/layout-chat/index.vue @@ -9,8 +9,11 @@ > <div v-if="chatWidget.isOpenChat || chatWidget.isClosing" - class="fixed right-0 z-[55] border surface-ground surface-border transition-transform ease-in-out max-md:w-full max-md:h-full max-md:top-0 max-md:right-0" - :class="chatClass" + :class="[ + ...chatClass, + { 'pointer-events-none': showAuthOverlay }, + 'fixed right-0 z-[55] border surface-ground surface-border transition-transform ease-in-out max-md:w-full max-md:h-full max-md:top-0 max-md:right-0' + ]" @transitionend="onTransitionEnd" > <div class="h-full flex flex-col"> @@ -32,22 +35,61 @@ </div> </div> </Transition> + + <!-- Auth overlay - only show for basic auth --> + <Transition + enter-active-class="transition-all duration-300 ease-out" + enter-from-class="opacity-0" + enter-to-class="opacity-100" + leave-active-class="transition-all duration-300 ease-in" + leave-from-class="opacity-100" + leave-to-class="opacity-0" + > + <div v-if="showAuthOverlay && chatWidget.authMode === 'basic'" + class="fixed inset-0 z-[60] bg-black/50 flex items-center justify-center"> + <BasicAuthWindow :onSubmit="handleAuthSubmit" /> + </div> + </Transition> </template> <script setup> - import { computed, inject } from 'vue' + import { computed, inject, ref } from 'vue' import ChatHeader from './chat-header.vue' import ChatBody from './chat-body.vue' import ChatFooter from './chat-footer.vue' + import BasicAuthWindow from '../BasicAuthWindow.vue' import { useAzionCopilot } from '../../composables/useAzionCopilot' + import { AuthService } from '../../services/auth' + import { CONSTANTS } from '../../core' const chatWidget = inject('chatWidget') + const showAuthOverlay = ref(false) defineOptions({ name: 'layout-chat' }) - const { messages, sendMessage, cancelMessage, resetChat, isProcessingRequest, sendFeedback } = + const { messages, sendMessage, cancelMessage, resetChat, isProcessingRequest, sendFeedback, copilot } = useAzionCopilot({ server: chatWidget.serverUrl }) + copilot.on(CONSTANTS.EVENTS.AUTH_REQUIRED, () => { + console.log(chatWidget) + if (chatWidget.authMode === 'basic') { + showAuthOverlay.value = true + } + }) + + const handleAuthSubmit = async (password) => { + const authService = new AuthService({ + authMode: 'basic', + copilotBackend: chatWidget.serverUrl.url + }) + + const result = await authService.fetchBasicAuth(password) + if (result.token) { + copilot.setAuthToken(result.token) + showAuthOverlay.value = false + } + } + const closeChat = () => { chatWidget.isClosing = true } diff --git a/templates/vue/vue3-ai-chatbot-widget/src/composables/useAzionCopilot.js b/templates/vue/vue3-ai-chatbot-widget/src/composables/useAzionCopilot.js index fdb0fa41..f147e7ff 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/composables/useAzionCopilot.js +++ b/templates/vue/vue3-ai-chatbot-widget/src/composables/useAzionCopilot.js @@ -3,6 +3,11 @@ import { AzionCopilot, CONSTANTS } from '../core' export function useAzionCopilot(config) { const copilot = new AzionCopilot(config) + + const token = sessionStorage.getItem('copilot_auth_token') + if (token) { + copilot.setAuthToken(token) + } const messages = ref([]) const isProcessingRequest = ref(false) @@ -40,6 +45,7 @@ export function useAzionCopilot(config) { sendFeedback, resetChat, isProcessingRequest, - cancelMessage + cancelMessage, + copilot } } diff --git a/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js b/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js index 5b74b7e1..565e5aa9 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js +++ b/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js @@ -6,6 +6,7 @@ export class AzionCopilot { this.messages = [] this.sessionId = crypto.randomUUID() this.events = new EventEmitter() + this.authToken = null this.serverConfig = { ...CONSTANTS.SERVER.DEFAULT, @@ -163,9 +164,18 @@ export class AzionCopilot { this.currentRequest = new AbortController() try { + const headers = { + 'Content-Type': 'application/json', + } + + if (this.authToken) { + headers['Authorization'] = `Bearer ${this.authToken}` + } + const response = await fetch(`${this.serverConfig.url}${this.serverConfig.conversation}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + headers, body: JSON.stringify({ messages: messageQueue, stream: this.config.stream, @@ -175,6 +185,10 @@ export class AzionCopilot { }) if (!response.ok) { + if (response.status === 401) { + this.events.emit(CONSTANTS.EVENTS.AUTH_REQUIRED) + throw new Error('Authentication required') + } systemMessage.status = CONSTANTS.STATUS.MESSAGES.ERROR systemMessage.content = CONSTANTS.MESSAGES.SYSTEM.ERROR @@ -258,9 +272,18 @@ export class AzionCopilot { comments } + const headers = { + 'Content-Type': 'application/json', + } + + if (this.authToken) { + headers['Authorization'] = `Bearer ${this.authToken}` + } + const response = await fetch(`${this.serverConfig.url}${this.serverConfig.feedback}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + headers, body: JSON.stringify(feedbackData) }) @@ -338,4 +361,8 @@ export class AzionCopilot { on(event, callback) { return this.events.on(event, callback) } + + setAuthToken(token) { + this.authToken = token + } } diff --git a/templates/vue/vue3-ai-chatbot-widget/src/core/constants.js b/templates/vue/vue3-ai-chatbot-widget/src/core/constants.js index 51c08fc2..aa2fad25 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/core/constants.js +++ b/templates/vue/vue3-ai-chatbot-widget/src/core/constants.js @@ -33,6 +33,7 @@ export const CONSTANTS = { ERROR: 'error', CLEAR: 'clear', CANCEL: 'cancel', - FEEDBACK: 'feedback' + FEEDBACK: 'feedback', + AUTH_REQUIRED: 'auth_required' } } diff --git a/templates/vue/vue3-ai-chatbot-widget/src/main.js b/templates/vue/vue3-ai-chatbot-widget/src/main.js index 7346355d..f559772a 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/main.js +++ b/templates/vue/vue3-ai-chatbot-widget/src/main.js @@ -15,6 +15,7 @@ import PrimeVue from 'primevue/config' import Tooltip from 'primevue/tooltip' import ToastService from 'primevue/toastservice' import App from './App.vue' +import { AuthService } from './services/auth' const getConfigDefaults = () => ({ theme: import.meta.env.VITE_THEME || 'light', @@ -33,19 +34,48 @@ const getConfigDefaults = () => ({ isOpenByDefault: true, isMaximizedByDefault: true, previewText: import.meta.env.VITE_PREVIEW_TEXT || '', - footerDisclaimer: import.meta.env.VITE_FOOTER_DISCLAIMER || '' + footerDisclaimer: import.meta.env.VITE_FOOTER_DISCLAIMER || '', + clerkPublicKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY, + authMode: import.meta.env.VITE_AUTH_MODE }) const CONFIG_DEFAULT = getConfigDefaults() -const app = createApp(App, CONFIG_DEFAULT) - document.documentElement.className = `azion azion-${CONFIG_DEFAULT.theme}` - document.title = CONFIG_DEFAULT.title || 'Copilot' -app.use(PrimeVue) -app.directive('tooltip', Tooltip) -app.use(ToastService) +async function init() { + const authService = new AuthService({ + authMode: CONFIG_DEFAULT.authMode, + copilotBackend: CONFIG_DEFAULT.serverUrl.url, + clerkPublicKey: CONFIG_DEFAULT.clerkPublicKey + }) + + try { + const user = await authService.signIn() + if (user) { + mountMainApp() + } + } catch (error) { + console.error('Authentication error:', error) + } +} + +function mountMainApp() { + const app = createApp(App, CONFIG_DEFAULT) + app.use(PrimeVue) + app.directive('tooltip', Tooltip) + app.use(ToastService) + app.mount('#app') +} + +// Add this to handle the redirect +if (window.location.hash.includes('__clerk_status=active')) { + console.log('Detected Clerk redirect, reloading...') + window.location.hash = '' + window.location.reload() +} else { + init() +} + -app.mount('#app') diff --git a/templates/vue/vue3-ai-chatbot-widget/src/services/auth.js b/templates/vue/vue3-ai-chatbot-widget/src/services/auth.js new file mode 100644 index 00000000..4a0e234d --- /dev/null +++ b/templates/vue/vue3-ai-chatbot-widget/src/services/auth.js @@ -0,0 +1,116 @@ +import { Clerk } from '@clerk/clerk-js' +import { createApp, defineAsyncComponent } from 'vue' + +export class AuthService { + constructor(args) { + this.authMode = ['clerk', 'basic', 'none'].includes(args.authMode) ? args.authMode : 'none' + this.copilotBackend = args.copilotBackend + this.clerkPublicKey = args.clerkPublicKey + } + + async signIn(password = null) { + if (this.authMode === 'clerk') { + return this.clerkSignIn() + } + + if (this.authMode === 'basic') { + if (sessionStorage.getItem('copilot_auth_token')) { + return { authenticated: true, token: sessionStorage.getItem('copilot_auth_token') } + } + + if (password) { + return this.basicSignIn(password) + } + return this.basicSignIn() + } + + return {} + } + + async clerkSignIn() { + + const clerk = new Clerk(this.clerkPublicKey) + await clerk.load() + + if (!clerk.user) { + clerk.openSignIn({ + appearance: { + socialButtonsVariant: "iconButton", + elements: { + card: { + boxShadow: 'none', + }, + modalCloseButton: { + display: 'none' + }, + footerAction: { + display: 'none' + }, + } + }, + }) + + await new Promise(resolve => { + clerk.addListener(({ user }) => { + if (user) resolve() + }) + }) + } + return clerk.user + } + + async fetchBasicAuth(password) { + if (!password) { + throw new Error('Password required') + } + + const response = await fetch(`${this.copilotBackend}/auth`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${password}` + }, + body: JSON.stringify({ + messages: [{ + "role": "user", + "content": "Authenticate" + }] + }) + }) + const token = await response.text() + + if (response.status !== 200 || !token) { + throw new Error('Authentication failed') + } + sessionStorage.setItem('copilot_auth_token', token) + return { authenticated: true, token: token} + } + + async basicSignIn() { + sessionStorage.removeItem('copilot_auth_token') + return new Promise((resolve, reject) => { + const modalContainer = document.createElement('div') + document.body.appendChild(modalContainer) + + const BasicAuthWindow = defineAsyncComponent(() => + import('../components/BasicAuthWindow.vue') + ) + + const authApp = createApp(BasicAuthWindow, { + 'onSubmit': async (password) => { + try { + const user = await this.fetchBasicAuth(password) + authApp.unmount() + document.body.removeChild(modalContainer) + resolve(user) + } catch (error) { + throw error + } + } + }) + + authApp.mount(modalContainer) + }) + } +} \ No newline at end of file From f4cff1b1de035d9d739ecdb36a18bfa7d88603cf Mon Sep 17 00:00:00 2001 From: PedroMiolaSilva <pedro.silva@azion.com> Date: Wed, 19 Feb 2025 09:29:30 -0300 Subject: [PATCH 2/4] update env example --- templates/vue/vue3-ai-chatbot-widget/.env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/vue/vue3-ai-chatbot-widget/.env.example b/templates/vue/vue3-ai-chatbot-widget/.env.example index 8bf453ca..593e0257 100644 --- a/templates/vue/vue3-ai-chatbot-widget/.env.example +++ b/templates/vue/vue3-ai-chatbot-widget/.env.example @@ -18,3 +18,10 @@ VITE_PREVIEW_TEXT= # VITE_FOOTER_DISCLAIMER: Footer disclaimer text VITE_FOOTER_DISCLAIMER= + +# VITE_AUTH_MODE: Authentication mode +# Options: basic or clerk. +VITE_AUTH_MODE= + +# VITE_CLERK_PUBLIC_KEY: Clerk public key +VITE_CLERK_PUBLIC_KEY= From 26693bd7278a87cd17386ce841cbe57acb8c403d Mon Sep 17 00:00:00 2001 From: PedroMiolaSilva <pedro.silva@azion.com> Date: Wed, 19 Feb 2025 17:34:56 -0300 Subject: [PATCH 3/4] refactor: changing from system to assistant --- .../src/components/message-item.vue | 2 +- .../src/core/azion-copilot.js | 38 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue b/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue index 8a02b140..3b551bf2 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue +++ b/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue @@ -149,7 +149,7 @@ } }) - const isSystem = computed(() => props.message.role === 'system') + const isSystem = computed(() => props.message.role === 'assistant') const messageReadingStatus = computed( () => props.message.status === CONSTANTS.STATUS.MESSAGES.RESPONDING ) diff --git a/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js b/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js index 565e5aa9..934ebdf0 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js +++ b/templates/vue/vue3-ai-chatbot-widget/src/core/azion-copilot.js @@ -29,7 +29,7 @@ export class AzionCopilot { id: msg.id || crypto.randomUUID(), status: CONSTANTS.STATUS.MESSAGES.COMPLETED, feedback: - msg.role === 'system' + msg.role === 'assistant' ? (msg.feedback ?? { completed: false, rating: CONSTANTS.STATUS.FEEDBACK.NEUTRAL }) : msg.feedback })) @@ -54,9 +54,9 @@ export class AzionCopilot { } } - createSystemMessage() { + createAssistantMessage() { return { - role: 'system', + role: 'assistant', content: '', status: CONSTANTS.STATUS.MESSAGES.RESPONDING, feedback: { @@ -156,10 +156,10 @@ export class AzionCopilot { async sendMessage(content) { const userMessage = this.createInitialMessage(content) - const systemMessage = this.createSystemMessage() + const assistantMessage = this.createAssistantMessage() const messageQueue = [...this.messages, userMessage] - this.messages = [...this.messages, userMessage, systemMessage] + this.messages = [...this.messages, userMessage, assistantMessage] this.emitMessagesUpdate() this.currentRequest = new AbortController() @@ -189,21 +189,21 @@ export class AzionCopilot { this.events.emit(CONSTANTS.EVENTS.AUTH_REQUIRED) throw new Error('Authentication required') } - systemMessage.status = CONSTANTS.STATUS.MESSAGES.ERROR - systemMessage.content = CONSTANTS.MESSAGES.SYSTEM.ERROR + assistantMessage.status = CONSTANTS.STATUS.MESSAGES.ERROR + assistantMessage.content = CONSTANTS.MESSAGES.SYSTEM.ERROR - this.messages[this.messages.length - 1] = { ...systemMessage } + this.messages[this.messages.length - 1] = { ...assistantMessage } this.emitMessagesUpdate() throw new Error(`HTTP error! status: ${response.status}`) } if (this.config.stream) { - await this.handleStreamResponse(response, systemMessage) + await this.handleStreamResponse(response, assistantMessage) } else { const data = await response.json() const completedMessage = { - ...systemMessage, + ...assistantMessage, content: data.content, status: CONSTANTS.STATUS.MESSAGES.COMPLETED } @@ -213,11 +213,11 @@ export class AzionCopilot { this.emitMessagesUpdate() } - return systemMessage + return assistantMessage } catch (error) { if (error.name !== 'AbortError') { const errorMessage = { - ...systemMessage, + ...assistantMessage, status: CONSTANTS.STATUS.MESSAGES.ERROR } @@ -234,17 +234,17 @@ export class AzionCopilot { this.currentRequest?.abort() this.currentRequest = null - const lastSystemMessage = [...this.messages] + const lastAssistantMessage = [...this.messages] .reverse() - .find((m) => m.role === 'system' && m.status === CONSTANTS.STATUS.MESSAGES.RESPONDING) + .find((m) => m.role === 'assistant' && m.status === CONSTANTS.STATUS.MESSAGES.RESPONDING) - if (lastSystemMessage) { - lastSystemMessage.status = CONSTANTS.STATUS.MESSAGES.CANCELED - lastSystemMessage.content += '\n' + if (lastAssistantMessage) { + lastAssistantMessage.status = CONSTANTS.STATUS.MESSAGES.CANCELED + lastAssistantMessage.content += '\n' - const index = this.messages.findIndex((m) => m.id === lastSystemMessage.id) + const index = this.messages.findIndex((m) => m.id === lastAssistantMessage.id) if (index !== -1) { - this.messages[index] = { ...lastSystemMessage } + this.messages[index] = { ...lastAssistantMessage } } this.emitMessagesUpdate() From fe8b3abca8a23c1ed885c6cb80c5a4509a342ca0 Mon Sep 17 00:00:00 2001 From: PedroMiolaSilva <pedro.silva@azion.com> Date: Wed, 19 Feb 2025 17:47:32 -0300 Subject: [PATCH 4/4] refactor: changing from system to assistant --- .../src/components/message-item.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue b/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue index 3b551bf2..f5c08168 100644 --- a/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue +++ b/templates/vue/vue3-ai-chatbot-widget/src/components/message-item.vue @@ -4,13 +4,13 @@ :class="classRoleApply" > <div - v-if="isSystem" + v-if="isAssistant" class="flex gap-3 mt-1" > <Avatar /> </div> <div class="message-content"> - <div v-if="!isSystem"> + <div v-if="!isAssistant"> <div v-html="formattedMessage" class="formatted-content" @@ -149,14 +149,14 @@ } }) - const isSystem = computed(() => props.message.role === 'assistant') + const isAssistant = computed(() => props.message.role === 'assistant') const messageReadingStatus = computed( () => props.message.status === CONSTANTS.STATUS.MESSAGES.RESPONDING ) const classRole = { user: 'surface-300 ml-auto break-words w-fit rounded-lg h-fit px-4 py-3', - system: 'mr-auto w-full mt-3' + assistant: 'mr-auto w-full mt-3' } const classRoleApply = classRole[props.message.role]