Skip to content

Commit

Permalink
feat(kb): Individual page and file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
RezaRahemtola committed Aug 1, 2024
1 parent d7a2f27 commit 4b91b96
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 43 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"dompurify": "^3.1.6",
"filesize": "^10.1.4",
"highlight.js": "^11.10.0",
"localforage": "^1.10.0",
"marked": "^12.0.2",
Expand Down
7 changes: 7 additions & 0 deletions public/icons/svg/arrow-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions public/icons/svg/arrow-left_lighten.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/boot/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';

// @ts-expect-error
import workerSrc from 'pdfjs-dist/build/pdf.worker?worker&url';
import * as pdfjs from 'pdfjs-dist';

export default boot(() => {
dayjs.extend(localizedFormat);
dayjs.extend(relativeTime);

pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
});
4 changes: 2 additions & 2 deletions src/components/MessageInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

<script lang="ts" setup>
import { ref } from 'vue';
import { processFile } from 'src/utils/attachments';
import { processAttachment } from 'src/utils/knowledge/attachments';
import { MessageAttachment, SendMessageParams } from 'src/types/chats';
import { useQuasar } from 'quasar';
Expand Down Expand Up @@ -92,7 +92,7 @@ const processMessageAttachments = async (event: any) => {
await Promise.all(
Array.from(target.files as FileList).map(async (file) => {
try {
const fileData = await processFile(file);
const fileData = await processAttachment(file);
attachmentsData.push(fileData);
} catch (error) {
$q.notify({
Expand Down
159 changes: 159 additions & 0 deletions src/pages/KnowledgeBase.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<template>
<section v-if="knowledgeBaseRef" class="max-sm:tw-mx-4 md:tw-mx-10 tw-space-y-4 tw-my-5">
<q-btn
:icon="`img:icons/svg/arrow-left${$q.dark.mode ? '_lighten' : ''}.svg`"
class="tw-w-10 tw-h-10"
to="/knowledge-base"
unelevated
/>
<div>
<h4 class="text-h4 text-semibold">{{ knowledgeBaseRef.name }}</h4>
<div class="tw-mt-4 tw-flex md:tw-justify-end">
<q-btn
class="border-primary-highlight"
icon="img:icons/svg/add.svg"
label="Upload document"
no-caps
rounded
unelevated
@click="($refs.documentUpload as any).click()"
/>
<!-- Hidden document upload -->
<input ref="documentUpload" accept=".txt,.md,.pdf" hidden multiple type="file" @change="uploadDocuments" />
</div>
</div>

<div class="tw-space-y-4">
<div
v-for="document of knowledgeBaseRef.documents"
:key="document.id"
class="tw-flex tw-border tw-items-center tw-rounded-lg tw-p-4"
>
<q-icon
:color="$q.dark.mode ? 'primary-dark' : 'purple-700'"
class="tw-h-5 tw-w-5 tw-mr-4"
name="img:icons/svg/attachment.svg"
/>

<p class="tw-font-bold tw-text-base">{{ document.name }}</p>
<div class="tw-ml-auto tw-flex tw-items-center tw-gap-4">
<p>{{ filesize(document.size, { round: 0 }) }}</p>
<q-btn class="tw-w-10 tw-h-10" disable icon="img:icons/svg/chat.svg" unelevated />

<q-btn class="tw-w-10 tw-h-10" unelevated @click="downloadDocument(document)">
<q-icon :color="$q.dark.mode ? 'primary-dark' : 'purple-700'" name="download" />
</q-btn>

<q-btn class="tw-w-10 tw-h-10" disable unelevated>
<q-icon :color="$q.dark.mode ? 'primary-dark' : 'purple-700'" name="more_horiz" />
</q-btn>
</div>
</div>
</div>
</section>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { KnowledgeBase, KnowledgeDocument } from 'src/types/knowledge';
import { useKnowledgeStore } from 'stores/knowledge';
import { exportFile, useQuasar } from 'quasar';
import { useAccount } from '@wagmi/vue';
import { useAccountStore } from 'stores/account';
import { processDocument } from 'src/utils/knowledge/document';
import { filesize } from 'filesize';
const $q = useQuasar();
const route = useRoute();
const router = useRouter();
const account = useAccount();
const accountStore = useAccountStore();
const knowledgeStore = useKnowledgeStore();
const knowledgeBaseRef = ref<KnowledgeBase | undefined>(undefined);
watch(
() => route.params.id as string,
async (newId: string) => {
await loadKnowledgeBase(newId);
},
{ immediate: true },
);
async function loadKnowledgeBase(id: string) {
if (!account.isConnected.value) {
$q.notify({ message: 'Account not connected', color: 'negative' });
await router.push({ path: '/' });
return;
}
if (!knowledgeStore.isLoaded) {
// This page was loaded directly, auto connect allowed us to get the wallet,
// but we didn't load KB yet, waiting for it
setTimeout(() => {
loadKnowledgeBase(id);
}, 100);
return;
}
const knowledgeBase = knowledgeStore.knowledgeBases.find((kb) => kb.id === id);
if (!knowledgeBase) {
$q.notify({ message: 'Knowledge base not found', color: 'negative' });
await router.push({ path: '/knowledge-base' });
return;
}
// Set the ref with a copy to avoid modifying the store value
knowledgeBaseRef.value = JSON.parse(JSON.stringify(knowledgeBase));
}
const uploadDocuments = async (event: any) => {
if (knowledgeBaseRef.value === undefined) {
return;
}
if (accountStore.alephStorage === null) {
$q.notify({
message: 'Connect your wallet to upload a document',
color: 'negative',
});
return;
}
const target = event.target as HTMLInputElement;
const documents: KnowledgeDocument[] = [];
await Promise.all(
Array.from(target.files as FileList).map(async (file) => {
try {
const document = await processDocument(file);
const uploadedFileMessage = await accountStore.alephStorage!.uploadFile(file);
documents.push({ ...document, store_hash: uploadedFileMessage.content.item_hash });
} catch (error) {
$q.notify({
message: (error as Error)?.message ?? 'Document processing failed, please try again',
color: 'negative',
});
}
}),
);
knowledgeBaseRef.value.documents = knowledgeBaseRef.value.documents.concat(documents);
await knowledgeStore.updateKnowledgeBase(
knowledgeBaseRef.value.id,
JSON.parse(JSON.stringify(knowledgeBaseRef.value)),
);
};
const downloadDocument = async (document: KnowledgeDocument) => {
if (accountStore.alephStorage === null) {
return;
}
const downloadedFile = await accountStore.alephStorage.downloadFile(document.store_hash);
exportFile(document.name, downloadedFile);
};
</script>
5 changes: 4 additions & 1 deletion src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const routes = [
name: 'new-chat',
component: () => import('pages/NewChat.vue'),
},
// chat view with chat id
{
path: 'chat/:id',
name: 'chat',
Expand All @@ -25,6 +24,10 @@ const routes = [
path: 'knowledge-base',
component: () => import('pages/KnowledgeBasesList.vue'),
},
{
path: 'knowledge-base/:id',
component: () => import('pages/KnowledgeBase.vue'),
},
{
path: 'persona-management',
component: () => import('pages/PersonaManagement.vue'),
Expand Down
14 changes: 14 additions & 0 deletions src/stores/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { useAccountStore } from 'stores/account';

type KnowledgeStoreState = {
knowledgeBases: KnowledgeBase[];
isLoaded: boolean;
};

export const useKnowledgeStore = defineStore('knowledge', {
state: (): KnowledgeStoreState => ({
knowledgeBases: [],
isLoaded: false,
}),
actions: {
async load() {
Expand All @@ -20,6 +22,7 @@ export const useKnowledgeStore = defineStore('knowledge', {
}

const knowledgeBases = await alephStorage.fetchKnowledgeBases();
this.isLoaded = true;
if (knowledgeBases === undefined) {
return;
}
Expand All @@ -41,5 +44,16 @@ export const useKnowledgeStore = defineStore('knowledge', {

await this.saveOnAleph();
},

async updateKnowledgeBase(id: string, kb: KnowledgeBase) {
this.knowledgeBases = this.knowledgeBases.map((knowledgeBase) => {
if (knowledgeBase.id === id) {
return kb;
}
return knowledgeBase;
});

await this.saveOnAleph();
},
},
});
12 changes: 11 additions & 1 deletion src/types/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { z } from 'zod';

const knowledgeDocumentSchema = z.object({
id: z.string().uuid(),
name: z.string(),
type: z.string(),
content: z.string(),
size: z.number(),
store_hash: z.string(),
});
export type KnowledgeDocument = z.infer<typeof knowledgeDocumentSchema>;

export const knowledgeSchema = z.object({
id: z.string().uuid(),
name: z.string(),
documents: z.array(z.any()),
documents: z.array(knowledgeDocumentSchema),
lastUpdatedAt: z.string().datetime(),
});
export type KnowledgeBase = z.infer<typeof knowledgeSchema>;
Expand Down
4 changes: 4 additions & 0 deletions src/utils/aleph-persistent-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export class AlephPersistentStorage {
return message;
}

async downloadFile(hash: string) {
return this.subAccountClient.downloadFile(hash);
}

async fetchKnowledgeBases(): Promise<KnowledgeBase[] | undefined> {
try {
const response = await this.subAccountClient.fetchAggregate(this.account.address, LIBERTAI_KNOWLEDGE_BASE_KEY);
Expand Down
16 changes: 16 additions & 0 deletions src/utils/knowledge/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { v4 as uuidv4 } from 'uuid';
import { MessageAttachment } from 'src/types/chats';
import { extractFileContent } from 'src/utils/knowledge/parsing';

export const processAttachment = async (file: File): Promise<MessageAttachment> => {
const title = file.name;
const fileInfo = await extractFileContent(file);

if (fileInfo.content.length > 4 * 1024) {
// File is too big to be inlined, rejecting it.
// Later we'll use a knowledge db to fix this.
throw new Error('File is too big, please use a file of 4 KB of content or less.');
}

return { title, content: fileInfo.content, type: fileInfo.content, id: uuidv4() };
};
10 changes: 10 additions & 0 deletions src/utils/knowledge/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { v4 as uuidv4 } from 'uuid';

import { KnowledgeDocument } from 'src/types/knowledge';
import { extractFileContent } from 'src/utils/knowledge/parsing';

export const processDocument = async (file: File): Promise<Omit<KnowledgeDocument, 'store_hash'>> => {
const fileInfo = await extractFileContent(file);

return { ...fileInfo, id: uuidv4(), name: file.name, size: file.size };
};
Loading

0 comments on commit 4b91b96

Please sign in to comment.