Skip to content

Commit

Permalink
chore: initial chatbot iframe page
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanorosanelli committed Nov 19, 2024
1 parent e8a2493 commit e62e619
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 1 deletion.
2 changes: 1 addition & 1 deletion middleware/auth.global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
'/privacy/cookie-policy',
);
}
if (publicPages.includes(to.path)) {
if (publicPages.includes(to.path) || to.path.startsWith('/chatbot-iframe/')) {
return;
}

Expand Down
273 changes: 273 additions & 0 deletions pages/chatbot-iframe/[id].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<template>
<main>
<div v-if="collection.uuid" class="space-y-12">
<div class="flex justify-between items-start space-x-4">
<div class="space-y-4">
<h2 class="text-2xl md:text-3xl leading-tight font-bold">{{ collection.cmetadata?.title }}</h2>
<div v-if="collection.cmetadata?.description" class="text-neutral-600" v-html="collection.cmetadata?.description"></div>
</div>
</div>

<div v-if="dialog.length">
<!-- <hr class="my-6 border-neutral-300"> -->
<div class="px-4 pt-6 pb-4 bg-white shadow-md rounded space-y-3">
<div class="flex flex-col space-y-6 pb-4">
<div
v-for="(item, i) in dialog"
:key="i"
class="chat-balloon space-y-2"
:class="{ 'bg-pink-800': item.error }"
@mouseover="
showResponseMenu = true;
hovered = i;
"
@mouseleave="showResponseMenu = false"
>
<div class="flex space-x-3 justify-between">
<p class="text-xs">{{ item.who }}</p>
<div class="chat-balloon-status" :class="{ busy: isBusy && i === dialog.length - 1 }"></div>
</div>
<div class="break-words rich-text" v-html="formatResponse(item.message, responseFormat)"></div>
</div>
</div>
</div>
</div>

<div class="space-y-4">
<div class="flex space-x-4">
<input
ref="input"
v-model.trim="prompt"
type="text"
class="grow text-lg p-2 rounded border border-sky-500 disabled:bg-neutral-100 disabled:border-neutral-300 shadow-md disabled:shadow-none"
:disabled="isBusy || messagesLeft == '0'"
@keydown.enter="submit"
/>
<button class="px-6 button shadow-md disabled:shadow-none" :disabled="isBusy || messagesLeft == '0'" @click="submit">
<span class="sm:hidden">›</span>
<span class="hidden sm:inline">{{ $t('SEND') }}</span>
</button>
</div>
</div>
</div>
</main>
</template>

<script lang="ts" setup>
const config = useRuntimeConfig();
const store = useStatesStore();
const { formatResponse, llmResponseFormat } = useResponseFormat();
interface DialogItem {
who: string;
message: string;
error: boolean;
}
const { t } = useI18n();
const collection = ref<{ name?: string; uuid?: string; cmetadata?: any }>({});
const isBusy = ref(false);
const prompt = ref('');
const input = ref<HTMLElement | null>(null);
const dialog = ref<DialogItem[]>([]);
const docs = ref<any>([]);
const historyId = ref('');
const canSeeDocs = ref(false);
let docsJsonString = '';
let responseEnded = false;
let currIdx = 0;
const isDemo = ref(store.userHasRole('demo'));
const messagesLeft = ref('');
const hovered = ref(-1);
const showResponseMenu = ref(true);
let sessionId = '';
let collectionName = '';
let editLevel = ItemEditLevel.None;

Check failure on line 88 in pages/chatbot-iframe/[id].vue

View workflow job for this annotation

GitHub Actions / Check code using Prettier and ESLint (ubuntu-latest, 20)

'editLevel' is assigned a value but never used. Allowed unused vars must match /^_/u
const responseFormat = ref('text');
onBeforeMount(async () => {
const route = useRoute();
collectionName = route.params.id as string;
// check if user has access to this page (TODO: refactor to use middleware)
const link = `/chatbot/${collectionName}`;
store.userAccess(link);
const item = store.getMenuItem(link);
editLevel = item?.edit || ItemEditLevel.None;
isBusy.value = true;
const data = await $fetch(`/api/brevia/collections?name=${collectionName}`);
collection.value = data;
if (!collection.value?.uuid) {
throw createError({
statusCode: 404,
message: 'not found',
fatal: true,
});
}
responseFormat.value = llmResponseFormat(collection.value.cmetadata?.qa_completion_llm);
sessionId = crypto.randomUUID();
isBusy.value = false;
updateLeftMessages();
});
watch(isBusy, (val) => {
if (!val) {
setTimeout(() => {
input.value?.focus();
}, 100);
}
});
// methods
const formatDialogItem = (who: string, message: string, error = false): DialogItem => {
return {
who,
message,
error,
};
};
const submit = async () => {
if (!prompt.value) return;
isBusy.value = true;
dialog.value.push(formatDialogItem('YOU', prompt.value));
dialog.value.push(formatDialogItem('BREVIA', ''));
currIdx = dialog.value.length - 1;
try {
await streamingFetchRequest();
isBusy.value = false;
} catch (error) {
isBusy.value = false;
showErrorInDialog(currIdx);
console.log(error);
}
};
const streamingFetchRequest = async () => {
const question = prompt.value;
prompt.value = '';
docs.value = [];
historyId.value = '';
docsJsonString = '';
responseEnded = false;
canSeeDocs.value = false;
const response = await fetch('/api/brevia/chat', {
method: 'POST',
headers: {
'Content-type': 'application/json',
'X-Chat-Session': sessionId,
},
body: JSON.stringify({
question,
collection: collectionName,
source_docs: true,
streaming: true,
}),
});
const reader = response?.body?.getReader();
if (reader) {
for await (const chunk of readChunks(reader)) {
const text = new TextDecoder().decode(chunk);
handleStreamText(text);
}
parseDocsJson();
await updateLeftMessages();
}
await updateLeftMessages();
};
const readChunks = (reader: ReadableStreamDefaultReader) => {
return {
async *[Symbol.asyncIterator]() {
let readResult = await reader.read();
while (!readResult.done) {
yield readResult.value;
readResult = await reader.read();
}
},
};
};
const handleStreamText = (text: string) => {
if (text.includes('[{"chat_history_id":') || text.includes('[{"page_content":')) {
const idx1 = text.indexOf('[{"chat_history_id":');
const idx2 = text.indexOf('[{"page_content":');
const idx = Math.max(idx1, idx2);
dialog.value[currIdx].message += text.slice(0, idx);
responseEnded = true;
docsJsonString += text.slice(idx);
} else if (responseEnded) {
docsJsonString += text;
} else if (text.startsWith('{"error":')) {
try {
const err = JSON.parse(text);
console.error(`Error response from API "${err?.error}"`);
showErrorInDialog(currIdx);
} catch (e) {
return console.error(e);
}
} else {
dialog.value[currIdx].message += text;
}
};
const parseDocsJson = () => {
try {
if (!docsJsonString) {
console.error('No docs found in response');
dialog.value[currIdx].error = true;
return;
}
const parsed = JSON.parse(docsJsonString);
if (parsed?.[0]?.chat_history_id) {
const item = parsed?.shift() || {};
historyId.value = item?.chat_history_id || '';
}
docs.value = parsed;
canSeeDocs.value = true;
} catch (e) {
return console.error(e);
}
};
const showErrorInDialog = (index: number) => {
const dialogItem = formatDialogItem('BREVIA', t('SOMETHING_WENT_WRONG'), true);
if (index) {
dialog.value[index] = dialogItem;
return;
}
dialog.value.push(dialogItem);
};
const updateLeftMessages = async () => {
if (!isDemo.value) {
return;
}
const today = new Date().toISOString().substring(0, 10);
const query = `min_date=${today}&collection=${collection.value?.name}`;
try {
const response = await fetch(`/api/brevia/chat_history?${query}`);
const data = await response.json();
const numItems = data?.meta?.pagination?.count || 0;
const left = Math.max(0, parseInt(config.public.demo.maxChatMessages) - parseInt(numItems));
messagesLeft.value = String(left);
} catch (error) {
console.log(error);
}
};
</script>

0 comments on commit e62e619

Please sign in to comment.