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';