feat: Interface to validate response_source (#8894)

- This PR adds a UI to validate the response source quality quickly. It also helps to test with sample questions and update responses in the database when missing.

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2024-02-26 20:20:12 +05:30
committed by GitHub
parent 77e463990a
commit 773be6f8ec
23 changed files with 514 additions and 57 deletions

View File

@@ -1 +1,28 @@
import 'chart.js';
import Vue from 'vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
Vue.use(VueDOMPurifyHTML);
const PlaygroundIndex = () =>
import('../superadmin_pages/views/playground/Index.vue');
const ComponentMapping = {
PlaygroundIndex: PlaygroundIndex,
};
const renderComponent = (componentName, props) => {
Vue.component(componentName, ComponentMapping[componentName]);
new Vue({
data: { props: props },
template: `<${componentName} :component-data="props"/>`,
}).$mount('#app');
};
document.addEventListener('DOMContentLoaded', () => {
const element = document.getElementById('app');
if (element) {
const componentName = element.dataset.componentName;
const props = JSON.parse(element.dataset.props);
renderComponent(componentName, props);
}
});

View File

@@ -0,0 +1,17 @@
<script setup>
defineProps({
message: {
type: String,
required: true,
},
});
</script>
<template>
<div class="w-full mb-4 flex items-center justify-start">
<div
v-dompurify-html="message"
class="px-4 py-3 bg-white max-w-4xl text-slate-700 leading-6 text-sm rounded-md inline-block border border-slate-100"
/>
</div>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
defineProps({
responseSourcePath: {
type: String,
required: true,
},
responseSourceName: {
type: String,
required: true,
},
});
</script>
<template>
<header
class="flex items-center px-8 py-4 bg-white border-b border-slate-100"
role="banner"
>
<a :href="responseSourcePath" class="text-woot-500 hover:underline mr-4">
Back
</a>
<div
class="border border-solid border-slate-100 text-slate-700 mr-4 p-2 rounded-full"
>
<svg width="24" height="24"><use xlink:href="#icon-mist-fill" /></svg>
</div>
<div class="flex flex-col h-14 justify-center">
<h1 id="page-title" class="text-base font-medium text-slate-900">
Robin AI playground
</h1>
<p class="text-sm text-slate-600">
Chat with the source
<span class="font-medium">
{{ responseSourceName }}
</span>
and evaluate its efficiency.
</p>
</div>
</header>
</template>

View File

@@ -0,0 +1,13 @@
<script setup>
import TypingIndicator from './assets/typing.gif';
</script>
<template>
<div class="w-full mb-4 flex items-center justify-start">
<div
class="px-2 py-2 bg-white max-w-4xl text-slate-700 leading-6 text-sm rounded-md inline-block border border-slate-100"
>
<img :src="TypingIndicator" alt="TypingIndicator" class="h-4" />
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
defineProps({
message: {
type: String,
required: true,
},
});
</script>
<template>
<div class="w-full mb-4 flex items-center justify-end">
<div
v-dompurify-html="message"
class="px-4 py-3 bg-woot-400 text-white text-sm rounded-md inline-block"
/>
</div>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,139 @@
<template>
<section class="h-full w-full flex flex-col bg-slate-25">
<PlaygroundHeader
:response-source-name="componentData.responseSourceName"
:response-source-path="componentData.responseSourcePath"
/>
<div ref="chatContainer" class="flex-1 overflow-auto px-8 py-4">
<div
v-for="message in messages"
:id="`message-${message.id}`"
:key="message.id"
>
<UserMessage
v-if="message.type === 'User'"
:message="formatMessage(message.content)"
/>
<BotMessage v-else :message="formatMessage(message.content)" />
</div>
<TypingIndicator v-if="isWaiting" />
</div>
<div class="w-full px-8 py-6">
<textarea
ref="messageInput"
v-model="messageContent"
:rows="4"
class="resize-none block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border !outline-2 border-slate-100 focus:ring-woot-500 focus:border-woot-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-woot-500 dark:focus:border-woot-500"
placeholder="Type a message... [CMD/CTRL + Enter to send]"
autofocus
autocomplete="off"
/>
</div>
</section>
</template>
<script>
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Header from '../../components/playground/Header.vue';
import UserMessage from '../../components/playground/UserMessage.vue';
import BotMessage from '../../components/playground/BotMessage.vue';
import TypingIndicator from '../../components/playground/TypingIndicator.vue';
export default {
components: {
PlaygroundHeader: Header,
UserMessage,
BotMessage,
TypingIndicator,
},
mixins: [messageFormatterMixin],
props: {
componentData: {
type: Object,
default: () => ({}),
},
},
data() {
return { messages: [], messageContent: '', isWaiting: false };
},
computed: {
previousMessages() {
return this.messages.map(message => ({
type: message.type,
message: message.content,
}));
},
},
mounted() {
this.focusInput();
document.addEventListener('keydown', this.handleKeyEvents);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyEvents);
},
methods: {
handleKeyEvents(e) {
const keyCode = buildHotKeys(e);
if (['meta+enter', 'ctrl+enter'].includes(keyCode)) {
this.onMessageSend();
}
},
focusInput() {
this.$refs.messageInput.focus();
},
onMessageSend() {
this.addMessageToData('User', this.messageContent);
this.sendMessageToServer(this.messageContent);
},
scrollToLastMessage() {
this.$nextTick(() => {
const messageId = this.messages[this.messages.length - 1].id;
const messageElement = document.getElementById(`message-${messageId}`);
messageElement.scrollIntoView({ behavior: 'smooth' });
});
},
addMessageToData(type, content) {
this.messages.push({ id: this.messages.length, type, content });
this.scrollToLastMessage();
},
async sendMessageToServer(messageContent) {
this.messageContent = '';
this.isWaiting = true;
const csrfToken = document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({
message: messageContent,
previous_messages: this.previousMessages,
}),
credentials: 'include',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const { message } = await response.json();
this.addMessageToData('Bot', message);
} catch (error) {
this.addMessageToData(
'bot',
'Error: Could not retrieve response. Please check the console for more details.'
);
} finally {
this.isWaiting = false;
this.focusInput();
}
},
},
};
</script>