stock-management-demo/src/components/ChatInterface.vue

168 lines
No EOL
4.2 KiB
Vue

<template>
<div class="q-pa-md column full-height">
<q-scroll-area
ref="scrollAreaRef"
class="col"
style="flex-grow: 1; overflow-x: visible; overflow-y: auto;"
>
<div class="q-mb-sm q-mx-md">
<q-chat-message
name="ASSISTANT"
bg-color="grey-4"
text-color="black"
>
<div class="message-content">
<p>Hi there, I'm StyleAI!<br>How can I help today?</p>
</div>
</q-chat-message>
</div>
<div
v-for="(message, index) in messages"
:key="index"
class="q-mb-sm q-mx-md"
>
<q-chat-message
:name="message.sender.toUpperCase()"
:sent="message.sender === 'user'"
:bg-color="message.sender === 'user' ? 'primary' : 'grey-4'"
:text-color="message.sender === 'user' ? 'white' : 'black'"
>
<!-- Use v-html to render parsed markdown -->
<div
v-if="!message.loading"
v-html="parseMarkdown(message.content)"
class="message-content"
/>
<!-- Optional: Add a spinner for a better loading visual -->
<template
v-if="message.loading"
#default
>
<q-spinner-dots size="2em" />
</template>
</q-chat-message>
</div>
</q-scroll-area>
<q-separator />
<div class="q-pa-sm row items-center">
<q-input
v-model="newMessage"
outlined
dense
placeholder="Type a message..."
class="col"
@keyup.enter="sendMessage"
autogrow
/>
<q-btn
round
dense
flat
icon="send"
color="primary"
class="q-ml-sm"
@click="sendMessage"
:disable="!newMessage.trim()"
/>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { QScrollArea, QChatMessage, QSpinnerDots } from 'quasar'; // Import QSpinnerDots
import { marked } from 'marked'; // Import marked
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps({
messages: {
type: Array,
required: true,
'default': () => [],
// Example message structure:
// { sender: 'Bot', content: 'Hello!', loading: false }
// { sender: 'You', content: 'Thinking...', loading: true }
},
});
const emit = defineEmits(['send-message']);
const newMessage = ref('');
const scrollAreaRef = ref(null);
const scrollToBottom = () =>
{
if (scrollAreaRef.value)
{
const scrollTarget = scrollAreaRef.value.getScrollTarget();
const duration = 300; // Optional: animation duration
// Use getScrollTarget().scrollHeight for accurate height
scrollAreaRef.value.setScrollPosition('vertical', scrollTarget.scrollHeight, duration);
}
};
const sendMessage = () =>
{
const trimmedMessage = newMessage.value.trim();
if (trimmedMessage)
{
emit('send-message', trimmedMessage);
newMessage.value = '';
// Ensure the scroll happens after the message is potentially added to the list
nextTick(() =>
{
scrollToBottom();
});
}
};
const parseMarkdown = (content) =>
{
// Basic check to prevent errors if content is not a string
if (typeof content !== 'string')
{
return '';
}
// Configure marked options if needed (e.g., sanitization)
// marked.setOptions({ sanitize: true }); // Example: Enable sanitization
content = marked(content);
//Find any anchor tags which go to `/mantis/$MANTIS_ID` and give them an onclick to call `window.openMantis($MANTIS_ID)` instead.
content = content.replace(/<a href="\/mantis\/(\d+)"/g, (match, mantisId) =>
{
return `<a class='cursor-pointer' onclick="window.openMantis(${mantisId})"`;
});
//Set all anchor tags to open in new tab
content = content.replace(/<a /g, '<a target="_blank" rel="noopener noreferrer" ');
return content;
};
// Scroll to bottom when messages change or component mounts
watch(() => props.messages, () =>
{
nextTick(() =>
{
scrollToBottom();
});
}, { deep: true, immediate: true });
window.openMantis = (ticketId) =>
{
router.push({ name: 'mantis', params: { ticketId } });
};
</script>
<style>
.message-content p {
margin: 0;
padding: 0;
}
</style>