Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

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,75 @@
<script setup>
import { computed } from 'vue';
import BarChart from 'shared/components/charts/BarChart.vue';
const props = defineProps({
componentData: {
type: Object,
default: () => ({}),
},
});
const prepareData = sourceData => {
var labels = [];
var data = [];
sourceData.forEach(item => {
labels.push(item[0]);
data.push(item[1]);
});
return {
labels,
datasets: [
{
type: 'bar',
backgroundColor: 'rgb(31, 147, 255)',
yAxisID: 'y',
label: 'Conversations',
data: data,
},
],
};
};
const chartData = computed(() => {
return prepareData(props.componentData.chartData);
});
const { accountsCount, usersCount, inboxesCount, conversationsCount } =
props.componentData;
</script>
<template>
<div class="w-full h-full">
<header class="main-content__header" role="banner">
<h1 id="page-title" class="main-content__page-title">
{{ 'Admin Dashboard' }}
</h1>
</header>
<section class="main-content__body main-content__body--flush">
<div class="report--list">
<div class="report-card">
<div class="metric">{{ accountsCount }}</div>
<div>{{ 'Accounts' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ usersCount }}</div>
<div>{{ 'Users' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ inboxesCount }}</div>
<div>{{ 'Inboxes' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ conversationsCount }}</div>
<div>{{ 'Conversations' }}</div>
</div>
</div>
</section>
<!-- eslint-disable vue/no-static-inline-styles -->
<BarChart
class="p-8 w-full"
:collection="chartData"
style="max-height: 500px"
/>
</div>
</template>

View File

@@ -0,0 +1,135 @@
<script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import PlaygroundHeader 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,
UserMessage,
BotMessage,
TypingIndicator,
},
props: {
componentData: {
type: Object,
default: () => ({}),
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return { messages: [], messageContent: '', isWaiting: false };
},
computed: {
previousMessages() {
return this.messages.map(message => ({
type: message.type,
message: message.content,
}));
},
},
mounted() {
this.focusInput();
},
methods: {
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>
<template>
<section class="flex flex-col w-full h-full bg-slate-25">
<PlaygroundHeader
:response-source-name="componentData.responseSourceName"
:response-source-path="componentData.responseSourcePath"
/>
<div class="flex-1 px-8 py-4 overflow-auto">
<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"
@keydown.meta.enter="onMessageSend"
@keydown.ctrl.enter="onMessageSend"
/>
</div>
</section>
</template>