@@ -6,16 +6,144 @@ const routeId = computed(() => {
66 return Array .isArray (route .params .id ) ? route .params .id [0 ] : route .params .id ;
77});
88
9- const { knowledge, deleteKnowledge } = useKnowledgeStore ();
9+ const { isLoading, knowledge, deleteKnowledge } = useKnowledgeStore ();
1010const k = computed (() => {
1111 if (! routeId .value ) return null ;
12- if (! knowledge [routeId .value ]) navigateTo (" /knowledge" );
12+ if (! knowledge [routeId .value ] && ! isLoading ) navigateTo (" /knowledge" );
13+ // getDocuments(knowledge[routeId.value]?.name);
1314 return knowledge [routeId .value ];
1415});
1516
17+ // async function getDocuments(knowledgeName?: string) {
18+ // if (!knowledgeName) return;
19+ // const response = await $fetch("/api/knowledge/document", {
20+ // method: "GET",
21+ // params: {
22+ // knowledgeName,
23+ // type: "all",
24+ // },
25+ // });
26+ // if (!response) {
27+ // throw new Error("Failed to retrieve knowledge");
28+ // }
29+ // console.log("response:", response);
30+ // }
31+
1632const deleteKnowledgeModalVisible = ref (false );
1733const deleteKnowledgeConfirmation = ref (" " );
1834const deleteKnowledgeConfirmationRef = ref <HTMLElement | null >(null );
35+ const showAddDocuments = ref (false );
36+ const fileInputRef = ref <HTMLInputElement | null >(null );
37+ const dropZoneRef = ref <HTMLDivElement | null >(null );
38+ const pendingFiles = ref <File []>([]);
39+ const uploadProgress = ref <{
40+ fileName: string ;
41+ percent: number ;
42+ } | null >(null );
43+ const uploadStatus = ref (" " ); // Add a ref to display upload status
44+
45+ function onDrop(files : File [] | null ) {
46+ if (files && files .length > 0 ) {
47+ // Filter for accepted types if needed, or rely on backend validation
48+ pendingFiles .value .push (... files );
49+ }
50+ }
51+ const { isOverDropZone } = useDropZone (dropZoneRef , {
52+ onDrop ,
53+ // You might want to allow more types like '.pdf', etc.
54+ multiple: true ,
55+ });
56+
57+ const triggerFileInput = () => {
58+ if (fileInputRef .value ) {
59+ fileInputRef .value .click ();
60+ }
61+ };
62+
63+ const handleFileChange = (event : Event ) => {
64+ const fileInput = event .target as HTMLInputElement ;
65+ if (fileInput .files && fileInput .files .length > 0 ) {
66+ const files = Array .from (fileInput .files );
67+ // Filter for accepted types if needed
68+ pendingFiles .value .push (... files );
69+ }
70+ };
71+
72+ async function uploadFiles() {
73+ if (! k .value ) return ;
74+ uploadStatus .value = " Uploading files..." ; // Set status to uploading
75+
76+ if (pendingFiles .value .length === 0 ) {
77+ uploadStatus .value = " No files selected to upload." ;
78+ console .warn (" No files to upload." );
79+ return ; // Stop the function if no files are present
80+ }
81+
82+ const formData = new FormData ();
83+ formData .append (" provider" , k .value .provider );
84+ formData .append (" dbName" , k .value .name );
85+
86+ // Loop through the array of files and append each one
87+ // using the same key name ("documents").
88+ for (const file of pendingFiles .value ) {
89+ formData .append (" documents" , file );
90+ }
91+
92+ // Handle the file upload
93+ try {
94+ // $fetch will correctly handle the FormData body for a POST request
95+ const response = await fetch (" /api/knowledge/document" , {
96+ method: " POST" ,
97+ body: formData ,
98+ });
99+
100+ if (! response .ok || ! response .body ) {
101+ throw new Error (` Failed to create DB: ${response .statusText } ` );
102+ }
103+
104+ const reader = response .body .getReader ();
105+ const decoder = new TextDecoder ();
106+ pendingFiles .value = []; // Clear the pending files after starting the upload
107+
108+ while (true ) {
109+ const { done, value } = await reader .read ();
110+ if (done ) break ;
111+
112+ // Decode the chunk and update the upload status
113+ const chunk = decoder .decode (value );
114+ const events = parseSSEChunk (chunk );
115+ for (const event of events ) {
116+ if (event .eventType === " progress" ) {
117+ uploadProgress .value = JSON .parse (event .data );
118+ } else if (event .eventType === " error" ) {
119+ uploadStatus .value = ` Error: ${event .data } ` ;
120+ } else if (event .eventType === " success" ) {
121+ const data = JSON .parse (event .data );
122+ const { updateKnowledge, fetchKnowledge } = useKnowledgeStore ();
123+ updateKnowledge (
124+ data .id ,
125+ data .dbName ,
126+ data .provider ,
127+ data .createdAt ,
128+ data .updatedAt ,
129+ data .documents ,
130+ data .chunks ,
131+ );
132+ await fetchKnowledge ();
133+ showAddDocuments .value = false ;
134+ }
135+ }
136+ }
137+ } catch (error ) {
138+ console .error (" Error uploading files:" , error );
139+ uploadStatus .value = ` Error uploading files: ${error } ` ; // Display error message
140+ } finally {
141+ // Clear the file input value regardless of success or failure
142+ if (fileInputRef .value ) {
143+ fileInputRef .value .value = " " ;
144+ }
145+ }
146+ }
19147 </script >
20148<template >
21149 <div class =" w-full h-full flex flex-col overflow-hidden" >
@@ -46,22 +174,118 @@ const deleteKnowledgeConfirmationRef = ref<HTMLElement | null>(null);
46174 {{ k?.updatedAt && new Date(k?.updatedAt).toLocaleDateString() }}
47175 {{ k?.updatedAt && new Date(k?.updatedAt).toLocaleTimeString() }}
48176 </div >
49- <div >documents: {{ k?.details.documents }}</div >
50- <div >chunks: {{ k?.details.chunks }}</div >
51- <button
52- class =" bg-(--error-color) rounded-lg"
53- @click ="
54- () => {
55- deleteKnowledgeModalVisible = true;
56- nextTick(() => {
57- deleteKnowledgeConfirmationRef?.focus();
58- });
59- }
60- "
61- >
62- <Icon name =" lucide:trash-2" class =" scale-125" />
63- delete
64- </button >
177+ <div >documents: {{ k?.documents }}</div >
178+ <div >chunks: {{ k?.chunks }}</div >
179+ <div class =" flex gap-2" >
180+ <button
181+ class =" bg-(--sub-color) rounded-lg"
182+ @click =" showAddDocuments = !showAddDocuments"
183+ >
184+ <Icon name =" lucide:file-plus" class =" scale-125" />
185+ add documents
186+ </button >
187+ <button
188+ class =" bg-(--error-color) rounded-lg"
189+ @click ="
190+ () => {
191+ deleteKnowledgeModalVisible = true;
192+ nextTick(() => {
193+ deleteKnowledgeConfirmationRef?.focus();
194+ });
195+ }
196+ "
197+ >
198+ <Icon name =" lucide:trash-2" class =" scale-125" />
199+ delete
200+ </button >
201+ </div >
202+ <div v-if =" showAddDocuments" >
203+ <div class =" h-[1px] bg-(--sub-color) my-4" />
204+ <div class =" flex flex-col gap-4" >
205+ <h4 >1. add files</h4 >
206+ <div
207+ class =" grid grid-cols-[1fr_min-content_2fr] items-center gap-4 h-[200px]"
208+ >
209+ <div
210+ class =" h-full flex items-center justify-center gap-3 bg-(--sub-color) rounded-lg cursor-pointer"
211+ @click =" triggerFileInput"
212+ @keydown.enter =" triggerFileInput"
213+ @keydown.space =" triggerFileInput"
214+ >
215+ <Icon
216+ name =" lucide:upload"
217+ class =" text-(--main-color) scale-150"
218+ />
219+ browse local files
220+ </div >
221+ <input
222+ ref =" fileInputRef"
223+ type =" file"
224+ multiple
225+ accept =" .txt,.pdf"
226+ class =" hidden"
227+ @change =" handleFileChange"
228+ />
229+ <div >or</div >
230+ <div
231+ ref =" dropZoneRef"
232+ class =" h-full flex items-center justify-center bg-(--sub-alt-color) rounded-lg border-3 border-dashed"
233+ :class ="
234+ isOverDropZone
235+ ? 'border-(--main-color)'
236+ : 'border-(--sub-color)'
237+ "
238+ >
239+ drag and drop files here
240+ </div >
241+ </div >
242+ </div >
243+ <div
244+ v-if =" pendingFiles.length"
245+ class =" h-[1px] bg-(--sub-color) my-4"
246+ />
247+ <div v-if =" pendingFiles.length" class =" flex flex-col gap-4" >
248+ <h4 >2. review files</h4 >
249+ <div
250+ v-for =" file in pendingFiles"
251+ :key =" file.name"
252+ class =" flex justify-between"
253+ >
254+ {{ file.name }}
255+ <Icon
256+ name =" lucide:x"
257+ class =" text-(--error-color) scale-150 cursor-pointer"
258+ @click ="
259+ () => {
260+ pendingFiles = pendingFiles.filter(
261+ (f) => f.name !== file.name,
262+ );
263+ }
264+ "
265+ />
266+ </div >
267+ <button
268+ class =" bg-(--main-color) text-(--bg-color) rounded-lg p-2"
269+ @click =" uploadFiles"
270+ >
271+ 3. upload files and create database
272+ </button >
273+ </div >
274+ <!-- Display the upload status -->
275+ <div v-if =" uploadProgress" class =" flex gap-3 items-center" >
276+ <div >{{ uploadProgress.fileName }}</div >
277+ <div class =" w-[200px] h-2 bg-(--sub-color) rounded-full" >
278+ <div
279+ class =" h-full bg-(--main-color) rounded-full"
280+ :style =" {
281+ width: (uploadProgress.percent || 0) + '%',
282+ }"
283+ />
284+ </div >
285+ <div >{{ uploadProgress.percent || 0 }}%</div >
286+ </div >
287+ <div v-if =" uploadStatus" class =" mt-2 text-sm" >{{ uploadStatus }}</div >
288+ </div >
65289 </div >
66290 </div >
67291
@@ -92,7 +316,7 @@ const deleteKnowledgeConfirmationRef = ref<HTMLElement | null>(null);
92316 @keyup.enter ="
93317 async () => {
94318 if (!k?.id) return;
95- await deleteKnowledge(k?.id);
319+ await deleteKnowledge(k?.id, k?.name );
96320 navigateTo('/knowledge');
97321 }
98322 "
@@ -108,7 +332,7 @@ const deleteKnowledgeConfirmationRef = ref<HTMLElement | null>(null);
108332 @click ="
109333 async () => {
110334 if (!k?.id) return;
111- await deleteKnowledge(k?.id);
335+ await deleteKnowledge(k?.id, k?.name );
112336 navigateTo('/knowledge');
113337 }
114338 "
0 commit comments