Merge branch 'develop' into 'featureApollo'
# Conflicts: # src/router/index.js # src/views/pages/KsSimilaritySearch.vue
This commit is contained in:
@@ -1,98 +1,322 @@
|
||||
<template>
|
||||
<div className="card">
|
||||
<div className="card">
|
||||
<DataTable v-model:filters="filters" :value="ksdocuments" paginator showGridlines :rows="10" dataKey="id"
|
||||
filterDisplay="menu" :loading="loading"
|
||||
:globalFilterFields="['ingestionInfo.metadata.KsApplicationName', 'ingestionInfo.metadata.KsFileSources', 'ingestionInfo.metadata.KsDocSource', 'ingestionStatus', 'ingestionDateFormat']">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-4 p-4 ">
|
||||
<span class="text-xl font-bold">KS Documents</span>
|
||||
<div class="flex items-center gap-2 flex-grow">
|
||||
<IconField class="flex-grow">
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['global'].value" placeholder="Keyword Search" />
|
||||
</IconField>
|
||||
</div>
|
||||
<Button icon="pi pi-plus" rounded raised @click="newKsDocument()" v-tooltip="'Create New Document'"
|
||||
class="mr-2" />
|
||||
<Button icon="pi pi-check-circle" rounded raised @click="startlngestion()"
|
||||
v-tooltip="'Start All documents Ingestion'" class="mr-8" :disabled="allDocumentsIngested"
|
||||
:class="{ 'p-button-danger': allDocumentsIngested }" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>No Records found</template>
|
||||
<template #loading>Loading Data. Please wait....</template>
|
||||
<!--Column field="id" header="id" sortable style="min-width: 12rem">
|
||||
<template #body="slotProps">
|
||||
<Tag>ksdocuments: {{ slotProps.data.id }}</Tag>
|
||||
<Tag>ksingestioninfo: {{ slotProps.data.ingestionInfo.id }}</Tag>
|
||||
</template>
|
||||
</Column-->
|
||||
<!--Column field="ingestionInfo.id" header="ksingestioninfo id" sortable style="min-width: 12rem" /-->
|
||||
<Column field="ingestionInfo.metadata.KsApplicationName" header="KSApplicationName" sortable
|
||||
style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
{{ data.ingestionInfo.metadata.KsApplicationName }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()" placeholder="Search by File" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ingestionInfo.metadata.KsFileSource" header="KsFileSource" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.ingestionInfo.metadata.KsFileSource }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()"
|
||||
placeholder="Search by File Name" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ingestionInfo.metadata.KsDocSource" header="KsDocSource" sortable style="min-width: 12rem">
|
||||
<template #body="{ data }">
|
||||
{{ data.ingestionInfo.metadata.KsDocSource }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()" placeholder="Search by File" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ingestionStatus" header="Status" sortable>
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.ingestionStatus" :severity="getStatus(slotProps.data)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Select v-model="filterModel.value" @change="filterCallback()" :options="statuses" placeholder="Select One"
|
||||
style="min-width: 12rem" :showClear="true">
|
||||
<template #option="{ option }">
|
||||
<Tag :value="option" :severity="getStatus({ ingestionStatus: option })" />
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="Ingestion Date" filterField="ingestionDateFormat" dataType="date" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
{{ formatDate(data.ingestionDate) }}
|
||||
</template>
|
||||
<template #filter="{ filterModel }">
|
||||
<DatePicker v-model="filterModel.value" dateFormat="mm/dd/yy" placeholder="mm/dd/yyyy"
|
||||
@change="updateFilterModel" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column headerStyle="width: 5rem; text-align: center" bodyStyle="text-align: center; overflow: visible">
|
||||
<template #body="slotProps">
|
||||
<div class="flex justify-center items-center space-x-3" >
|
||||
<!--Button type="button" icon="pi pi-pencil" rounded @click="editKsDocument(slotProps.data)"
|
||||
v-tooltip="'Edit the information of document'" /-->
|
||||
<Button type="button" icon="pi pi-play" rounded @click="startIndividualngestion(slotProps.data.id)"
|
||||
v-tooltip="'Start Ingestion of document'" :disabled="slotProps.data.ingestionStatus === 'INGESTED'"
|
||||
:class="{ 'p-button-danger': slotProps.data.ingestionStatus === 'INGESTED' }" />
|
||||
<Button type="button" icon="pi pi-trash" rounded @click="showConfirmDialog(slotProps.data.id)"
|
||||
v-tooltip="'Delete the ingested Record'" :disabled="slotProps.data.ingestionStatus === 'NEW'"
|
||||
:class="{ 'p-button-danger': slotProps.data.ingestionStatus === 'NEW' }" />
|
||||
|
||||
<DataTable :value="ksdocuments" :paginator="true" :rows="10" dataKey="id" :rowHover="true" showGridlines>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-xl font-bold">KS Documents</span>
|
||||
<Button icon="pi pi-plus" rounded raised @click="newKsDocument()" />
|
||||
</div>
|
||||
</template>
|
||||
<Column field="name" header="Name"></Column>
|
||||
<Column field="fileName" header="File Name"></Column>
|
||||
<Column field="ingestionStatus" header="Status">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.ingestionStatus" :severity="getStatus(slotProps.data)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ingestionDate" header="Ingestion Date"></Column>
|
||||
<Column headerStyle="width: 5rem; text-align: center" bodyStyle="text-align: center; overflow: visible">
|
||||
<template #body="slotProps">
|
||||
<Button type="button" icon="pi pi-pencil" rounded @click="editKsDocument(slotProps.data)" />
|
||||
<Tag :value="slotProps.data.id" />
|
||||
<Tag :value="slotProps.data.ingestionInfo.id" />
|
||||
<Button type="button" v-if="slotProps.data.ingestionStatus === 'NEW'" icon="pi pi-play" rounded
|
||||
@click="startIngestion(slotProps.data.id)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<Dialog header="Ingestion Result" v-model:visible="ingestionDialogVisible" :modal="true" :closable="false">
|
||||
<p>{{ ingestionResult }}</p>
|
||||
<Button label="OK" icon="pi pi-check" @click="ingestionDialogVisible = false" />
|
||||
</Dialog>
|
||||
</div>
|
||||
<Dialog header="Confirm Deletion" :visible="confirmDialogVisible" modal @hide="resetConfirmDialog"
|
||||
:style="{ width: '300px' }">
|
||||
<p>Are you sure you want to delete this record?</p>
|
||||
<template #footer>
|
||||
<Button label="No" icon="pi pi-times" @click="confirmDialogVisible = false"/>
|
||||
<Button label="Yes" icon="pi pi-check" @click="confirmDelete" class="p-button-danger" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
<Dialog header="Ingestion Result" v-model:visible="ingestionDialogVisible" :modal="true" :closable="false">
|
||||
<p>{{ ingestionResult }}</p>
|
||||
<Button label="OK" icon="pi pi-check" @click="ingestionDialogVisible = false" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FilterMatchMode, FilterOperator } from '@primevue/core/api';
|
||||
import axios from 'axios';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import moment from 'moment';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import Tag from 'primevue/tag';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const ksdocuments = ref(null);
|
||||
const loading = ref(true);
|
||||
|
||||
const ingestionDialogVisible = ref(false);
|
||||
const ingestionResult = ref('');
|
||||
const filters = ref();
|
||||
|
||||
const confirmDialogVisible = ref(false);
|
||||
const recordToDelete = ref(null);
|
||||
|
||||
const initFilters = () => {
|
||||
filters.value = {
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
|
||||
'ingestionInfo.metadata.KsApplicationName': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
'ingestionInfo.metadata.KsFileSource': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
'ingestionInfo.metadata.KsDocSource': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
ingestionDateFormat: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.DATE_IS }] },
|
||||
ingestionStatus: { operator: FilterOperator.OR, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] }
|
||||
};
|
||||
};
|
||||
|
||||
initFilters();
|
||||
|
||||
const statuses = ref(['NEW', 'INGESTED', 'FAILED']); // Add your statuses here
|
||||
|
||||
onMounted(() => {
|
||||
axios.get('http://localhost:8082/fe-api/ksdocuments')
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
ksdocuments.value = response.data;
|
||||
});
|
||||
axios.get('/fe-api/ksdocuments')
|
||||
.then(response => {
|
||||
ksdocuments.value = getCustomDatewithAllResponse(response.data);
|
||||
console.log(ksdocuments.value);
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Computed property to check if all documents are ingested
|
||||
const allDocumentsIngested = computed(() => {
|
||||
return ksdocuments.value && ksdocuments.value.every(doc => doc.ingestionStatus === 'INGESTED');
|
||||
});
|
||||
|
||||
|
||||
const getStatus = (data) => {
|
||||
if (data.ingestionStatus === 'INGESTED') {
|
||||
return 'success';
|
||||
} else if (data.ingestionStatus === 'NEW') {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'warn';
|
||||
}
|
||||
if (data.ingestionStatus === 'INGESTED') {
|
||||
return 'success';
|
||||
} else if (data.ingestionStatus === 'NEW') {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'warn';
|
||||
}
|
||||
}
|
||||
|
||||
const getCustomDatewithAllResponse = (data) => {
|
||||
return [...(data || [])].map((d) => {
|
||||
d.ingestionDateFormat = new Date(d.ingestionDateFormat);
|
||||
return d;
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterModel = () => {
|
||||
console.log("updateFilterModel")
|
||||
}
|
||||
|
||||
const editKsDocument = (data) => {
|
||||
console.log(data);
|
||||
router.push({ name: 'ks-document-edit', params: { id: data.id } });
|
||||
console.log(data);
|
||||
router.push({ name: 'ks-document-edit', params: { id: data.id } });
|
||||
}
|
||||
|
||||
const startIngestion = (id) => {
|
||||
axios.get(`http://localhost:8082/test/ingest_document/${id}`)
|
||||
//axios.get('http://localhost:8082/test/ingestion_loop')
|
||||
.then(response => {
|
||||
ingestionResult.value = response.data;
|
||||
if (response.data.status == "OK") {
|
||||
ksdocuments.value.forEach(element => {
|
||||
if (response.data.ingestedDocumentId.includes(element.id)) {
|
||||
element.status = "INGESTED"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ingestionResult.value = `Error: ${response.data.message}`;
|
||||
}
|
||||
//delete functionality
|
||||
function showConfirmDialog(id) {
|
||||
recordToDelete.value = id;
|
||||
confirmDialogVisible.value = true;
|
||||
}
|
||||
|
||||
ingestionDialogVisible.value = true;
|
||||
})
|
||||
.catch(error => {
|
||||
ingestionDialogVisible.value = true;
|
||||
});
|
||||
function confirmDelete() {
|
||||
if (recordToDelete.value !== null) {
|
||||
deleteRecordsFromVectorStore(recordToDelete.value);
|
||||
recordToDelete.value = null;
|
||||
}
|
||||
confirmDialogVisible.value = false;
|
||||
}
|
||||
|
||||
function resetConfirmDialog() {
|
||||
recordToDelete.value = null;
|
||||
}
|
||||
|
||||
|
||||
const deleteRecordsFromVectorStore = (id) => {
|
||||
const documentToDelete = ksdocuments.value.find(doc => doc.id === id);
|
||||
|
||||
if (!documentToDelete) {
|
||||
console.error('Document not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const requestPayload = {
|
||||
ksDocumentId: id,
|
||||
ksIngestionInfoId: documentToDelete.ingestionInfo.id,
|
||||
ksDoctype: documentToDelete.ingestionInfo.metadata.KsDoctype,
|
||||
ksDocSource: documentToDelete.ingestionInfo.metadata.KsDocSource,
|
||||
ksFileSource: documentToDelete.ingestionInfo.metadata.KsFileSource,
|
||||
ksApplicationName: documentToDelete.ingestionInfo.metadata.KsApplicationName,
|
||||
};
|
||||
|
||||
axios.post('/fe-api/vector-store/deleteRecords', requestPayload)
|
||||
.then(response => {
|
||||
console.log('Delete resource:', response.data)
|
||||
ksdocuments.value = ksdocuments.value.filter(doc => doc.id !== id);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting records: ', error)
|
||||
});
|
||||
}
|
||||
|
||||
//ingestion
|
||||
const startIndividualngestion = (id) => {
|
||||
axios.get(`/test/ingest_document/${id}`)
|
||||
//axios.get('/test/ingestion_loop')
|
||||
.then(response => {
|
||||
ingestionResult.value = response.data;
|
||||
if (response.data.status == "OK") {
|
||||
ksdocuments.value.forEach(element => {
|
||||
if (response.data.ingestedDocumentId.includes(element.id)) {
|
||||
element.status = "INGESTED"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ingestionResult.value = `Error: ${response.data.message}`;
|
||||
}
|
||||
|
||||
ingestionDialogVisible.value = true;
|
||||
})
|
||||
.catch(error => {
|
||||
ingestionDialogVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const startlngestion = () => {
|
||||
axios.get('/test/ingestion_loop')
|
||||
.then(response => {
|
||||
ingestionResult.value = response.data;
|
||||
if (response.data.status == "OK") {
|
||||
ksdocuments.value.forEach(element => {
|
||||
if (response.data.ingestedDocumentId.includes(element.id)) {
|
||||
element.status = "INGESTED"
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ingestionResult.value = `Error: ${response.data.message}`;
|
||||
}
|
||||
|
||||
ingestionDialogVisible.value = true;
|
||||
})
|
||||
.catch(error => {
|
||||
ingestionDialogVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
//new record creation
|
||||
const newKsDocument = () => {
|
||||
console.log('new');
|
||||
router.push({ name: 'ks-document-new' });
|
||||
console.log('new');
|
||||
router.push({ name: 'ks-document-new' });
|
||||
|
||||
}
|
||||
|
||||
// Function to format date string
|
||||
function formatDate(dateString) {
|
||||
// Parse the date string using moment
|
||||
return moment(dateString).format('MM/DD/YYYY');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add this to ensure buttons are spaced consistently */
|
||||
.space-x-3 > * + * {
|
||||
margin-left: 1rem; /* Adjust as needed for desired spacing */
|
||||
}
|
||||
|
||||
/* Custom styling for disabled red button */
|
||||
.p-button-danger {
|
||||
background-color: white;
|
||||
border-color: blue;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.p-button-danger:disabled {
|
||||
/*background-color: red;*/
|
||||
border-color: red;
|
||||
color: red;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,73 +6,84 @@
|
||||
<h2 class="text-3xl font-bold mb-4">Ks document</h2>
|
||||
</div>
|
||||
<form @submit.prevent="submitForm" class="p-fluid">
|
||||
<div class="lex flex-col md:flex-row gap-4">
|
||||
<div class="flex flex-wrap gap-2 w-full">
|
||||
<label for="description">Description</label>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="description" v-tooltip="'A brief overview of the system purpose and functionality.'">System
|
||||
Description</label>
|
||||
<InputText id="description" type="text" v-model="formData.description" required class="w-full" />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="type">Type</label>
|
||||
<label for="type"
|
||||
v-tooltip="'Specify the type of file here. e.g, PDF Document, DOCX, TXT, MD Document etc..'">File
|
||||
Type</label>
|
||||
<InputText id="type" v-model="formData.type" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksApplicationName">KS Application Name</label>
|
||||
<label for="ksApplicationName" v-tooltip="'Enter the application name here.'">KS Application Name</label>
|
||||
<InputText id="ksApplicationName" v-model="formData.ksApplicationName" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksDocType">KS Doc Type</label>
|
||||
<InputText id="ksDocType" v-model="formData.ksDocType" required class="w-full" />
|
||||
<label for="ksDocType" v-tooltip="'Specify the type of document e.g, md, pdf,'">KS Document Type</label>
|
||||
<Select id="ksDocType" v-model="formData.ksDocType" :options="dropdownItems" required optionLabel="name" optionValue="value" placeholder="Select One" class="w-full"></Select>
|
||||
<!--InputText id="ksDocType" v-model="formData.ksDocType" required class="w-full" /-->
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksDocSource">KS Doc Source</label>
|
||||
<label for="ksDocSource"
|
||||
v-tooltip="'The KS Document Source field is intended to capture the origin or source from where the document was obtained or associated. ex.. Retrieved from DevopsJ2Cloud Git Repository - CSV System Configuration '">KS
|
||||
Document Source</label>
|
||||
<InputText id="ksDocSource" v-model="formData.ksDocSource" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="defaultChunkSize">Default Chunk Size</label>
|
||||
<label for="defaultChunkSize" v-tooltip="'Define the default size for chunks of data.'">Default Chunk
|
||||
Size</label>
|
||||
<InputNumber id="defaultChunkSize" v-model="formData.defaultChunkSize" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="minChunkSize">Min Chunk Size</label>
|
||||
<label for="minChunkSize" v-tooltip="'Specify the minimum allowable size for chunks'">Min Chunk
|
||||
Size</label>
|
||||
<InputNumber id="minChunkSize" v-model="formData.minChunkSize" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="maxNumberOfChunks">Max Number of Chunks</label>
|
||||
<label for="maxNumberOfChunks" v-tooltip="'Set the maximum number of chunks allowed.'">Max Number of
|
||||
Chunks</label>
|
||||
<InputNumber id="maxNumberOfChunks" v-model="formData.maxNumberOfChunks" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="minChunkSizeToEmbed">Min Chunk Size to Embed</label>
|
||||
<label for="minChunkSizeToEmbed" v-tooltip="'Define the minimum chunk size that can be embedded.'">Min
|
||||
Chunk Size to
|
||||
Embed</label>
|
||||
<InputNumber id="minChunkSizeToEmbed" v-model="formData.minChunkSizeToEmbed" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-4">
|
||||
<label for="file" class="block text-lg mb-2">File</label>
|
||||
<label for="file" class="block text-lg mb-2" v-tooltip="'Upload the file here.'">File</label>
|
||||
<div class="flex align-items-center">
|
||||
<FileUpload ref="fileUpload" mode="basic" :maxFileSize="10000000" chooseLabel="Select File"
|
||||
<FileUpload ref="fileUpload" mode="basic" :maxFileSize="10000000000" chooseLabel="Select File"
|
||||
class="p-button-rounded" @select="onFileSelect" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,81 +92,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</Fluid>
|
||||
<!--div class="card-container">
|
||||
<form @submit.prevent="submitForm" class="p-fluid">
|
||||
<div class="grid">
|
||||
<div class="col-12 mb-4">
|
||||
<label for="file" class="block text-lg mb-2">File</label>
|
||||
<div class="flex align-items-center">
|
||||
<FileUpload
|
||||
ref="fileUpload"
|
||||
mode="basic"
|
||||
:maxFileSize="10000000"
|
||||
chooseLabel="Select File"
|
||||
class="p-button-rounded"
|
||||
@select="onFileSelect"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="description">Description</label>
|
||||
<InputText id="description" v-model="formData.description" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="type">Type</label>
|
||||
<InputText id="type" v-model="formData.type" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksApplicationName">KS Application Name</label>
|
||||
<InputText id="ksApplicationName" v-model="formData.ksApplicationName" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksDocType">KS Doc Type</label>
|
||||
<InputText id="ksDocType" v-model="formData.ksDocType" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="ksDocSource">KS Doc Source</label>
|
||||
<InputText id="ksDocSource" v-model="formData.ksDocSource" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="defaultChunkSize">Default Chunk Size</label>
|
||||
<InputNumber id="defaultChunkSize" v-model="formData.defaultChunkSize" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="minChunkSize">Min Chunk Size</label>
|
||||
<InputNumber id="minChunkSize" v-model="formData.minChunkSize" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="maxNumberOfChunks">Max Number of Chunks</label>
|
||||
<InputNumber id="maxNumberOfChunks" v-model="formData.maxNumberOfChunks" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 md:col-6 mb-4">
|
||||
<span class="p-float-label">
|
||||
<label for="minChunkSizeToEmbed">Min Chunk Size to Embed</label>
|
||||
<InputNumber id="minChunkSizeToEmbed" v-model="formData.minChunkSizeToEmbed" required class="w-full" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" label="Submit" class="p-button-rounded p-button-lg" />
|
||||
</form>
|
||||
</div-->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -163,17 +99,25 @@ import axios from 'axios';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const dropdownItems = ref([
|
||||
{ name: 'PDF', value: 'pdf' },
|
||||
{ name: 'MD', value: 'md' },
|
||||
{ name: 'DOCX', value: 'docx' },
|
||||
{ name: 'EXCEL', value: 'excel' }
|
||||
]);
|
||||
|
||||
const formData = ref({
|
||||
description: 'Test-UI-DevopsJ2CSystem',
|
||||
description: '', //Jenkins DevopsJ2Cloud System CSV configuration md file
|
||||
ingestionStatus: 'NEW',
|
||||
type: 'MD_DOCUMENT',
|
||||
ksApplicationName: 'jenkins',
|
||||
ksDocType: 'setup-documentation',
|
||||
ksDocSource: 'guide-for-techincal-setup',
|
||||
type: '', //.md file
|
||||
ksApplicationName: '', //Jenkins-DevopsJ2Cloud
|
||||
ksDocType: '',
|
||||
ksDocSource: '', //Git Repository - DevopsJ2Cloud CSV System Configuration
|
||||
defaultChunkSize: 1000,
|
||||
minChunkSize: 200,
|
||||
maxNumberOfChunks: 1000,
|
||||
@@ -206,7 +150,7 @@ const submitForm = async () => {
|
||||
formDataToSend.append('minChunkSizeToEmbed', formData.value.minChunkSizeToEmbed);
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8082/upload', formDataToSend, {
|
||||
const response = await axios.post('/upload', formDataToSend, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
|
||||
@@ -1,34 +1,24 @@
|
||||
<template>
|
||||
<Fluid>
|
||||
<div class="flex mt-6">
|
||||
<div class="card flex flex-col gap-4 w-full">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold mb-4">Similarity Search</h2>
|
||||
</div>
|
||||
<div class="flex flex-wrap">
|
||||
<!--label for="address">Address</label-->
|
||||
<Textarea id="query" v-model="query" rows="4" placeholder="Enter your query..." class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex flex-wrap gap-2 w-full">
|
||||
<Select id="type" v-model="dropdownItem" :options="dropdownItems" optionLabel="name"
|
||||
placeholder="Select type" class="w-full"></Select>
|
||||
<h2 class="text-4xl font-semibold text-center mb-4">Similarity Search</h2>
|
||||
<div class="similarity-search">
|
||||
<div class="card-container flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Textarea id="query" v-model="query" rows="6" placeholder="Enter your query..." class="input-textarea" />
|
||||
<div class="select-container">
|
||||
<!--SelectButton id="type" v-model="dropdownItem" :options="dropdownItems" optionLabel="name"
|
||||
class="select-button" /-->
|
||||
<InputText v-model="filterQuery" type="text" placeholder="Add filterQuery" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-field p-col-12 p-md-2">
|
||||
<Button label="Send" icon="pi pi-send" :fluid="false" @click="sendQuery" />
|
||||
</div>
|
||||
<Button label="Query" icon="pi pi-send" @click="sendQuery" class="send-button" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-6">
|
||||
<div class="results-container p-mt-4">
|
||||
<Card v-for="(result, index) in messages" :key="index" class="p-mb-3">
|
||||
<div v-if="messages.length > 0" class="results-container mt-6">
|
||||
<Card v-for="(result, index) in messages" :key="index" class="result-card">
|
||||
<template #content>
|
||||
<ScrollPanel style="width: 100%; max-height: 200px">
|
||||
<pre class="result-content">{{ result }}</pre>
|
||||
<ScrollPanel style="width: 100%; max-height: 400px">
|
||||
<CodeSnippet :code="dynamicCode" language="systemd" />
|
||||
</ScrollPanel>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -41,13 +31,18 @@
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import ScrollPanel from 'primevue/scrollpanel';
|
||||
import SelectButton from 'primevue/selectbutton'; // Import SelectButton
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref } from 'vue';
|
||||
import { watch, ref } from 'vue';
|
||||
import CodeSnippet from '@/components/CodeSnippet.vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const query = ref('');
|
||||
const dropdownItem = ref(null);
|
||||
const messages = ref([]);
|
||||
const toast = useToast();
|
||||
const dynamicCode = ref('');
|
||||
const filterQuery = ref("'KsApplicationName' == 'ATF'")
|
||||
|
||||
const dropdownItems = [
|
||||
{ name: 'Documentation', code: 'setup-documentation' },
|
||||
@@ -55,12 +50,16 @@ const dropdownItems = [
|
||||
{ name: 'Source code', code: 'sourcecode' }
|
||||
];
|
||||
|
||||
|
||||
const sendQuery = async () => {
|
||||
if (query.value.trim() !== '' && dropdownItem.value) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8082/test/query_vector?query="${query.value}"&type=${dropdownItem.value.code}`);
|
||||
const data = await response.json();
|
||||
const sendQuery = () => {
|
||||
if (query.value.trim() !== '' && filterQuery) {
|
||||
axios.get('/test/query_vector', {
|
||||
params: {
|
||||
query: query.value,
|
||||
filterQuery: filterQuery.value,
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
const data = response.data;
|
||||
console.log('API response:', data);
|
||||
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
@@ -69,52 +68,83 @@ const sendQuery = async () => {
|
||||
} else {
|
||||
toast.add({ severity: 'info', summary: 'Info', detail: 'No results found', life: 3000 });
|
||||
}
|
||||
} catch (error) {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending query:', error);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to send query', life: 3000 });
|
||||
}
|
||||
query.value = '';
|
||||
dropdownItem.value = null;
|
||||
});
|
||||
} else {
|
||||
toast.add({ severity: 'warn', summary: 'Warning', detail: 'Please enter a query and select a type', life: 3000 });
|
||||
}
|
||||
};
|
||||
|
||||
// Function to generate dynamic code snippet
|
||||
function generateDynamicCode() {
|
||||
const randomValue = messages.value.join(', ');
|
||||
return `[${randomValue}]`;
|
||||
}
|
||||
|
||||
watch(messages, (newMessages) => {
|
||||
dynamicCode.value = generateDynamicCode();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.similarity-search {
|
||||
max-width: 1200px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.input-textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
min-height: 150px;
|
||||
/* Increased height for better readability */
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.select-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.send-button {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.results-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
.result-card {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.p-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-select .p-select-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.p-inputtextarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
.p-scrollpanel {
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
98
src/views/pages/KsVectorData.vue
Normal file
98
src/views/pages/KsVectorData.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div className="card">
|
||||
<DataTable v-model:filters="filters" :value="vectorDetails" dataKey="id" :loading="loading" paginator showGridlines
|
||||
:rows="10" filterDisplay="menu"
|
||||
:globalFilterFields="['id', 'metadata.ksApplicationName', 'metadata.ksDocSource', 'metadata.ksDoctype', 'metadata.ksFileSource']">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-4 p-4 ">
|
||||
<span class="text-xl font-bold">Vector Data</span>
|
||||
<div class="flex items-center gap-2 flex-grow">
|
||||
<IconField class="flex-grow">
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="filters['global'].value" placeholder="Keyword Search" />
|
||||
</IconField>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>No Records found</template>
|
||||
<template #loading>Loading Data. Please wait.....</template>
|
||||
<Column field="id" header="Id" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.id }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()" placeholder="Search By id" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="metadata.ksApplicationName" header="KsApplicationName" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.metadata.ksApplicationName }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()"
|
||||
placeholder="Search By Application name" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="metadata.ksFileSource" header="KsFileSource" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.metadata.ksFileSource }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()"
|
||||
placeholder="Search By Source/Path" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="metadata.ksDocSource" header="KsDocSource" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.metadata.ksDocSource }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()"
|
||||
placeholder="Search By Document/Repo source" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="metadata.ksDoctype" header="KsDoctype" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ data.metadata.ksDoctype }}
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<InputText v-model="filterModel.value" type="text" @input="filterCallback()"
|
||||
placeholder="Search By Document/Source type" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { FilterMatchMode, FilterOperator } from '@primevue/core/api'
|
||||
import axios from 'axios';
|
||||
|
||||
const vectorDetails = ref(null);
|
||||
const loading = ref(true);
|
||||
const filters = ref();
|
||||
|
||||
onMounted(() => {
|
||||
axios.get('/fe-api/vector-store/details')
|
||||
.then(response => {
|
||||
vectorDetails.value = response.data;
|
||||
console.log(vectorDetails.value)
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
const initFilters = () => {
|
||||
filters.value = {
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
id: { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.EQUALS }] },
|
||||
'metadata.ksApplicationName': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
'metadata.ksDocSource': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
'metadata.ksDoctype': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] },
|
||||
'metadata.ksFileSource': { operator: FilterOperator.AND, constraints: [{ value: null, matchMode: FilterMatchMode.STARTS_WITH }] }
|
||||
};
|
||||
};
|
||||
|
||||
initFilters();
|
||||
</script>
|
||||
@@ -1,59 +1,81 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { computed, ref } from 'vue';
|
||||
import {useAuth } from '@websanova/vue-auth/src/v3.js';
|
||||
const auth = useAuth();
|
||||
|
||||
const { isDarkTheme } = useLayout();
|
||||
const email = ref('');
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const checked = ref(false);
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
return `/layout/images/${isDarkTheme ? 'logo-white' : 'logo-dark'}.svg`;
|
||||
return `/layout/images/${isDarkTheme ? 'logo-white' : 'logo-dark'}.svg`;
|
||||
});
|
||||
|
||||
const login = () => {
|
||||
console.log('Username: ', username.value);
|
||||
|
||||
auth.login({
|
||||
data: {
|
||||
"username": username.value,
|
||||
"password": password.value
|
||||
},
|
||||
redirect: '/ksdocuments',
|
||||
fetchUser: true,
|
||||
url: '/api/auth/login'
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<img :src="logoUrl" alt="Sakai logo" class="mb-8 w-24 shrink-0" />
|
||||
<div style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||
<div class="text-center mb-8">
|
||||
<img src="/demo/images/login/avatar.png" alt="Image" height="50" class="mb-4" />
|
||||
<div class="text-surface-900 dark:text-surface-0 text-3xl font-medium mb-4">Welcome, Isabel!</div>
|
||||
<span class="text-surface-600 dark:text-surface-200 font-medium">Sign in to continue</span>
|
||||
</div>
|
||||
<div
|
||||
class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
|
||||
<div>
|
||||
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">Email</label>
|
||||
<InputText id="email1" type="text" placeholder="Email address" class="w-full md:w-[30rem] mb-8" style="padding: 1rem" v-model="email" />
|
||||
<div
|
||||
style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
|
||||
<div class="text-center mb-8">
|
||||
<span class="text-surface-600 dark:text-surface-200 font-medium">Welcome to Apollo- The Knowledge
|
||||
Source</span>
|
||||
</div>
|
||||
|
||||
<label for="password1" class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">Password</label>
|
||||
<Password id="password1" v-model="password" placeholder="Password" :toggleMask="true" class="w-full mb-4" inputClass="w-full" :inputStyle="{ padding: '1rem' }"></Password>
|
||||
<div>
|
||||
<label for="email1"
|
||||
class="block text-surface-900 dark:text-surface-0 text-xl font-medium mb-2">Username</label>
|
||||
<InputText id="email1" type="text" placeholder="Username" class="w-full md:w-[30rem] mb-8"
|
||||
style="padding: 1rem" v-model="username" />
|
||||
|
||||
<div class="flex items-center justify-between mb-8 gap-8">
|
||||
<div class="flex items-center">
|
||||
<Checkbox v-model="checked" id="rememberme1" binary class="mr-2"></Checkbox>
|
||||
<label for="rememberme1">Remember me</label>
|
||||
</div>
|
||||
<a class="font-medium no-underline ml-2 text-right cursor-pointer" style="color: var(--primary-color)">Forgot password?</a>
|
||||
</div>
|
||||
<Button label="Sign In" class="w-full p-4 text-xl"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="password1"
|
||||
class="block text-surface-900 dark:text-surface-0 font-medium text-xl mb-2">Password</label>
|
||||
<Password id="password1" v-model="password" placeholder="Password" :toggleMask="true" class="w-full mb-4"
|
||||
inputClass="w-full" :inputStyle="{ padding: '1rem' }"></Password>
|
||||
|
||||
<!--div class="flex items-center justify-between mb-8 gap-8">
|
||||
<div class="flex items-center">
|
||||
<Checkbox v-model="checked" id="rememberme1" binary class="mr-2"></Checkbox>
|
||||
<label for="rememberme1">Remember me</label>
|
||||
</div>
|
||||
<a class="font-medium no-underline ml-2 text-right cursor-pointer"
|
||||
style="color: var(--primary-color)">Forgot password?</a>
|
||||
</div> -->
|
||||
<Button label="Sign In" @click="login" class="w-full p-4 text-xl"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pi-eye {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.pi-eye-slash {
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
transform: scale(1.6);
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user