UI Knowledge import

missing - image attachment - work_dir browser (backend implemented, WIP) - WHISPER (hurry up)

Alessandro committed Nov 7, 2024 at 05:40 UTC 9d6b769dc29ea43805d2a551b8b48710f9313470
4 files changed +182 -12
run_ui.py
+56
@@ -13,6 +13,7 @@ from python.helpers.files import get_abs_path
13 from python.helpers.print_style import PrintStyle
14 from python.helpers.dotenv import load_dotenv
15 from python.helpers import persist_chat, settings
16 +from werkzeug.utils import secure_filename
17
18
19 # initialize the internal Flask server
@@ -65,6 +66,61 @@ def requires_auth(f):
66 return decorated
67
68
69 +UPLOAD_FOLDER = os.path.join(os.getcwd(), 'work_dir', 'uploads')
70 +
71 +@app.route('/upload', methods=['POST'])
72 +async def upload_file():
73 + if 'file' not in request.files:
74 + return jsonify({'ok': False, 'message': 'No file part'}), 400
75 +
76 + files = request.files.getlist('file') # Handle multiple files
77 + saved_filenames = []
78 +
79 + for file in files:
80 + if file and allowed_file(file.filename): # Check file type
81 + filename = secure_filename(file.filename)
82 + file.save(os.path.join(UPLOAD_FOLDER, filename))
83 + saved_filenames.append(filename)
84 +
85 + return jsonify({'ok': True, 'filenames': saved_filenames}) # Return saved filenames
86 +
87 +
88 +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'txt', 'pdf', 'csv', 'html', 'json', 'md'}
89 +
90 +def allowed_file(filename):
91 + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
92 +
93 +
94 +@app.route("/import_knowledge", methods=["POST"])
95 +async def import_knowledge():
96 + if 'files[]' not in request.files:
97 + return jsonify({'ok': False, 'message': 'No files part'}), 400
98 +
99 + files = request.files.getlist('files[]')
100 + KNOWLEDGE_FOLDER = os.path.join(os.getcwd(), 'knowledge', 'custom', 'main')
101 +
102 + saved_filenames = []
103 +
104 + for file in files:
105 + if file:
106 + filename = secure_filename(file.filename)
107 + file.save(os.path.join(KNOWLEDGE_FOLDER, filename))
108 + saved_filenames.append(filename)
109 +
110 + return jsonify({'ok': True, 'message': "Knowledge Imported", 'filenames': saved_filenames})
111 +
112 +
113 +@app.route("/work_dir", methods=["GET"]) # Correct route
114 +async def browse_work_dir():
115 + work_dir = os.path.join(os.getcwd(), 'work_dir')
116 + try:
117 + files = [f for f in os.listdir(work_dir) if os.path.isfile(os.path.join(work_dir, f))]
118 + return jsonify({'ok': True, 'files': files})
119 + except FileNotFoundError:
120 + return jsonify({'ok': False, 'message': 'work_dir not found'}), 404
121 + except Exception as e:
122 + return jsonify({'ok': False, 'message': f'Error browsing work_dir: {str(e)}'}), 500
123 +
124 # handle default address, show demo html page from ./test_form.html
125 @app.route("/", methods=["GET"])
126 async def test_form():
webui/index.css
+8 -9
@@ -922,12 +922,12 @@ pre {
922 /* Image preview section */
923 .image-preview-section {
924 display: flex;
925 - gap: 8px;
926 - padding: 8px;
925 + gap: var(--spacing-md);
926 + padding: var(--spacing-sm) var(--spacing-sm) 0.37rem var(--spacing-sm);
927 overflow-x: auto;
928 background-color: var(--color-input);
929 border-radius: 8px;
930 - margin-bottom: 8px;
930 + margin-bottom: var(--spacing-xs);
931 }
932
933 .preview-item {
@@ -936,19 +936,18 @@ pre {
936 }
937
938 .preview-item img {
939 - width: 100px;
940 - height: 100px;
939 + max-height: 100px;
940 object-fit: cover;
942 - border-radius: 4px;
943 - border: 1px solid var(--color-border);
941 + border-radius: 8px;
942 + border: 1px solid var(--color-border-light);
943 }
944
945 .remove-image {
946 position: absolute;
947 top: -6px;
948 right: -6px;
950 - width: 20px;
951 - height: 20px;
949 + width: 18px;
950 + height: 18px;
951 border-radius: 50%;
952 background-color: var(--color-accent);
953 color: white;
webui/index.html
+30 -3
@@ -213,7 +213,7 @@
213 </svg>
214 </label>
215 <input type="file" id="file-input"
216 - accept=".jpg,.png,.tiff,.bmp"
216 + accept="png, jpg, jpeg, txt, pdf, csv, html, json, md"
217 multiple
218 style="display: none"
219 @change="handleFileUpload($event)">
@@ -247,8 +247,13 @@
247
248 <!-- Bottom row with text buttons -->
249 <div class="text-buttons-row">
250 - <button class="text-button"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"></path></svg>Import knowledge</button>
251 - <button class="text-button"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 122.88 89.09"><g><path stroke-width="3.5" stroke="currentColor" fill="none" d="M3.97,9.75l-3.93,8.73l122.77,0.01l-3.96-8.74H55.82c-1.59,0-2.88-1.29-2.88-2.88V0H11.97v6.87 c0,1.59-1.29,2.88-2.88,2.88H3.97L3.97,9.75L3.97,9.75z"></path><path stroke-width="3.5" stroke="currentColor" fill="none" d="M4.63,18.48H0l7.03,67.03c0.11,1.07,0.55,2.02,1.2,2.69c0.55,0.55,1.28,0.89,2.11,0.89h100.1 c0.82,0,1.51-0.33,2.05-0.87c0.68-0.68,1.13-1.67,1.28-2.79l9.1-66.94H4.63V18.48L4.63,18.48z"></path></g></svg>work_dir Browser</button>
250 + <button class="text-button" @click="loadKnowledge()"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"></path></svg>Import knowledge</button>
251 + <button class="text-button" @click="workDirModalProxy.openModal()">
252 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 123.37 92.59">
253 + <path d="m5.72,11.5l-3.93,8.73h119.77s-3.96-8.73-3.96-8.73h-60.03c-1.59,0-2.88-1.29-2.88-2.88V1.75H13.72v6.87c0,1.59-1.29,2.88-2.88,2.88h-5.12Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
254 + <path d="m6.38,20.23H1.75l7.03,67.03c.11,1.07.55,2.02,1.2,2.69.55.55,1.28.89,2.11.89h97.1c.82,0,1.51-.33,2.05-.87.68-.68,1.13-1.67,1.28-2.79l9.1-66.94H6.38Z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="5"></path>
255 + </svg>work_dir Browser</button>
256 +
257 <button class="text-button" @click="pauseAgent(!paused)">
258 <!-- Dynamic path that switches between pause and play icons -->
259 <template x-if="!paused">
@@ -373,6 +378,28 @@
378 </template>
379 </div>
380
381 + <div id="workDirModal" x-data="workDirModalProxy">
382 + <template x-teleport="body">
383 + <div x-show="isOpen" class="modal-overlay" @click.self="close()" x-transition>
384 + <div class="modal-container">
385 + <div class="modal-header">
386 + <h2>Work Directory</h2>
387 + </div>
388 + <div class="modal-content">
389 + <ul>
390 + <template x-for="file in files" :key="file">
391 + <li x-text="file"></li>
392 + </template>
393 + </ul>
394 + </div>
395 + <div class="modal-footer">
396 + <button @click="close()">Close</button>
397 + </div>
398 + </div>
399 + </div>
400 + </template>
401 + </div>
402 +
403 </body>
404
405 </html>
\ No newline at end of file
webui/index.js
+88
@@ -202,6 +202,94 @@ function setMessage(id, type, heading, content, temp, kvps = null) {
202 }
203
204
205 +async function handleFileUpload(event) {
206 + const files = event.target.files;
207 + const formData = new FormData();
208 + for (let i = 0; i < files.length; i++) {
209 + formData.append('file', files[i]);
210 + }
211 +
212 + const response = await fetch('/upload', {
213 + method: 'POST',
214 + body: formData,
215 + });
216 +
217 + const data = await response.json();
218 + if (!data.ok) {
219 + toast(data.message, "error");
220 + } else {
221 + toast("Files uploaded: " + data.filenames.join(", "), "success");
222 + }
223 +}
224 +
225 +
226 +window.loadKnowledge = async function() {
227 + const input = document.createElement('input');
228 + input.type = 'file';
229 + input.accept = '.txt,.pdf,.csv,.html,.json,.md';
230 + input.multiple = true;
231 +
232 + input.onchange = async () => {
233 + const formData = new FormData();
234 + for (let file of input.files) {
235 + formData.append('files[]', file);
236 + }
237 +
238 + const response = await fetch('/import_knowledge', {
239 + method: 'POST',
240 + body: formData,
241 + });
242 +
243 + const data = await response.json();
244 + if (!data.ok) {
245 + toast(data.message, "error");
246 + } else {
247 + toast("Knowledge files imported: " + data.filenames.join(", "), "success");
248 + }
249 + };
250 +
251 + input.click();
252 +}
253 +
254 +
255 +const workDirModalProxy = {
256 + isOpen: false,
257 + files: [],
258 +
259 + async openModal() { // Define openModal
260 + // Inside openModal, call the existing open method:
261 + await this.open(); // Or directly include the fetching logic here
262 + },
263 +
264 + async open() {
265 + const response = await sendJsonData('/work_dir');
266 + if (response.ok) {
267 + this.files = response.files;
268 + this.isOpen = true;
269 + } else {
270 + toast(response.message, 'error');
271 + }
272 + },
273 +
274 + close() {
275 + this.isOpen = false;
276 + }
277 +};
278 +
279 +// Make the proxy available globally
280 +window.workDirModalProxy = workDirModalProxy;
281 +
282 +// Ensure correct setup for Alpine.js x-data.
283 +window.workDirModal = function() {
284 + return workDirModalProxy; // Returns the proxy object for the Work Dir modal
285 +}
286 +
287 +
288 +document.addEventListener('alpine:init', () => {
289 + // Make workDirModalProxy available as an Alpine component/store
290 + Alpine.data('workDirModal', workDirModal);
291 +});
292 +
293
294 function adjustTextareaHeight() {
295 chatInput.style.height = 'auto';