mindroot 7.7.0__py3-none-any.whl → 8.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mindroot might be problematic. Click here for more details.

Files changed (35) hide show
  1. mindroot/coreplugins/admin/plugin_manager.py +126 -3
  2. mindroot/coreplugins/admin/plugin_manager_backup.py +615 -0
  3. mindroot/coreplugins/admin/router.py +3 -1
  4. mindroot/coreplugins/admin/server_router.py +8 -1
  5. mindroot/coreplugins/admin/static/js/plugin-advanced-install.js +83 -12
  6. mindroot/coreplugins/admin/static/js/plugin-index-browser.js +138 -10
  7. mindroot/coreplugins/admin/static/js/plugin-install-dialog.js +345 -0
  8. mindroot/coreplugins/admin/static/js/server-control.js +68 -6
  9. mindroot/coreplugins/agent/agent.py +4 -0
  10. mindroot/coreplugins/chat/models.py +0 -1
  11. mindroot/coreplugins/chat/router.py +31 -0
  12. mindroot/coreplugins/chat/services.py +24 -0
  13. mindroot/coreplugins/chat/static/css/dark.css +35 -0
  14. mindroot/coreplugins/chat/static/css/default.css +35 -0
  15. mindroot/coreplugins/chat/static/js/chatform.js +185 -0
  16. mindroot/coreplugins/env_manager/__init__.py +3 -0
  17. mindroot/coreplugins/env_manager/inject/admin.jinja2 +16 -0
  18. mindroot/coreplugins/env_manager/mod.py +228 -0
  19. mindroot/coreplugins/env_manager/router.py +40 -0
  20. mindroot/coreplugins/env_manager/static/css/env-manager.css +263 -0
  21. mindroot/coreplugins/env_manager/static/js/env-manager.js +380 -0
  22. mindroot/coreplugins/home/router.py +33 -2
  23. mindroot/coreplugins/home/static/css/enhanced.css +111 -5
  24. mindroot/coreplugins/home/templates/home.jinja2 +7 -4
  25. mindroot/lib/chatlog.py +5 -1
  26. mindroot/lib/streamcmd.py +139 -0
  27. mindroot/lib/templates.py +13 -2
  28. mindroot/server.py +12 -25
  29. mindroot-8.2.0.dist-info/METADATA +15 -0
  30. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/RECORD +34 -25
  31. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/WHEEL +1 -1
  32. mindroot-7.7.0.dist-info/METADATA +0 -310
  33. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/entry_points.txt +0 -0
  34. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/licenses/LICENSE +0 -0
  35. {mindroot-7.7.0.dist-info → mindroot-8.2.0.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,8 @@ class ServerControl extends BaseEl {
5
5
  static properties = {
6
6
  status: { type: String },
7
7
  loading: { type: Boolean },
8
- method: { type: String }
8
+ method: { type: String },
9
+ monitorActive: { type: Boolean }
9
10
  };
10
11
 
11
12
  static styles = css`
@@ -96,6 +97,11 @@ class ServerControl extends BaseEl {
96
97
  margin-left: 0.5rem;
97
98
  }
98
99
 
100
+ .monitor-status {
101
+ font-style: italic;
102
+ margin-left: 0.5rem;
103
+ }
104
+
99
105
  @keyframes spin {
100
106
  100% { transform: rotate(360deg); }
101
107
  }
@@ -110,6 +116,56 @@ class ServerControl extends BaseEl {
110
116
  this.status = '';
111
117
  this.loading = false;
112
118
  this.method = '';
119
+ this.monitorActive = false;
120
+ this.monitorIntervalId = null;
121
+ }
122
+
123
+ // Function to check if server is online
124
+ async checkServerStatus() {
125
+ try {
126
+ const response = await fetch('/admin/server/ping', {
127
+ method: 'GET',
128
+ cache: 'no-store',
129
+ headers: { 'Cache-Control': 'no-cache' }
130
+ });
131
+ return response.ok;
132
+ } catch (error) {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ // Start monitoring server status
138
+ startServerMonitor() {
139
+ if (this.monitorActive) return; // Don't start if already monitoring
140
+
141
+ this.monitorActive = true;
142
+ let checkCount = 0;
143
+ const maxChecks = 120; // Maximum 2 minutes of checking
144
+
145
+ this.monitorIntervalId = setInterval(async () => {
146
+ checkCount++;
147
+ this.status = `Checking server status${'.'.repeat(checkCount % 4)} (${checkCount}s)`;
148
+
149
+ const isOnline = await this.checkServerStatus();
150
+
151
+ if (isOnline) {
152
+ this.stopMonitoring();
153
+ this.status = 'Server is back online! Refreshing page...';
154
+ setTimeout(() => window.location.reload(), 1000);
155
+ } else if (checkCount >= maxChecks) {
156
+ this.stopMonitoring();
157
+ this.status = 'Server did not come back online after 2 minutes. Please refresh manually.';
158
+ }
159
+ }, 1000);
160
+ }
161
+
162
+ // Stop monitoring
163
+ stopMonitoring() {
164
+ if (this.monitorIntervalId) {
165
+ clearInterval(this.monitorIntervalId);
166
+ this.monitorIntervalId = null;
167
+ }
168
+ this.monitorActive = false;
113
169
  }
114
170
 
115
171
  async handleRestart() {
@@ -128,12 +184,9 @@ class ServerControl extends BaseEl {
128
184
  this.status = result.message;
129
185
  this.method = result.method;
130
186
 
131
- // For PM2 and mindroot methods, we'll reload after a delay
187
+ // Start monitoring server status
132
188
  if (result.method === 'pm2' || result.method === 'mindroot') {
133
- setTimeout(() => {
134
- this.status = 'Attempting to reconnect...';
135
- window.location.reload();
136
- }, 5000);
189
+ this.startServerMonitor();
137
190
  }
138
191
  } else {
139
192
  throw new Error(`${result.message}${result.method ? ` (${result.method})` : ''}`);
@@ -146,6 +199,12 @@ class ServerControl extends BaseEl {
146
199
  }
147
200
  }
148
201
 
202
+ disconnectedCallback() {
203
+ super.disconnectedCallback();
204
+ // Clean up interval when component is removed
205
+ this.stopMonitoring();
206
+ }
207
+
149
208
  async handleStop() {
150
209
  if (!confirm('Are you sure you want to stop the server? You will need to restart it manually unless using PM2.')) return;
151
210
 
@@ -200,7 +259,10 @@ class ServerControl extends BaseEl {
200
259
  <span class="material-icons">${this.method === 'pm2' ? 'cloud_queue' : 'terminal'}</span>
201
260
  ${this.method}
202
261
  </span>
262
+ ${this.monitorActive ? html`
263
+ <span class="monitor-status">Monitoring server status...</span>
203
264
  ` : ''}
265
+ ` : ''}
204
266
  </div>
205
267
  ` : ''}
206
268
  </div>
@@ -275,6 +275,10 @@ class Agent:
275
275
  if buffer[0] == '{':
276
276
  buffer = "[" + buffer
277
277
 
278
+ # happened with Qwen 3 for some reason
279
+ buffer = buffer.replace('}] <>\n\n[{','}, {')
280
+ buffer = buffer.replace('}] <>\n[{','}, {')
281
+
278
282
  commands, partial_cmd = parse_streaming_commands(buffer)
279
283
 
280
284
  if isinstance(commands, int):
@@ -21,4 +21,3 @@ class ImageMessagePart(BaseModel):
21
21
 
22
22
  # Use Union to create a discriminated union type
23
23
  MessageParts = Union[TextMessagePart, ImageMessagePart]
24
-
@@ -1,5 +1,6 @@
1
1
  from fastapi import APIRouter, HTTPException, Request, Response
2
2
  from fastapi.responses import HTMLResponse, RedirectResponse
3
+ from fastapi import File, UploadFile, Form
3
4
  from sse_starlette.sse import EventSourceResponse
4
5
  from .models import MessageParts
5
6
  from lib.providers.services import service, service_manager
@@ -15,6 +16,8 @@ from lib.providers.services import service, service_manager
15
16
  from lib.providers.commands import command_manager
16
17
  from lib.utils.debug import debug_box
17
18
  from lib.session_files import load_session_data, save_session_data
19
+ import os
20
+ import shutil
18
21
  from pydantic import BaseModel
19
22
 
20
23
  router = APIRouter()
@@ -189,3 +192,31 @@ async def run_task_route(request: Request, agent_name: str, task_request: TaskRe
189
192
  return {"status": "ok", "results": task_result, "full_results": full_results, "log_id": log_id}
190
193
 
191
194
 
195
+ @router.post("/chat/{log_id}/upload")
196
+ async def upload_file(request: Request, log_id: str, file: UploadFile = File(...)):
197
+ """
198
+ Upload a file and store it in a user-specific directory.
199
+ Returns the file path that can be used in messages.
200
+ """
201
+ user = request.state.user.username
202
+
203
+ # Create user uploads directory if it doesn't exist
204
+ user_upload_dir = f"data/users/{user}/uploads/{log_id}"
205
+ os.makedirs(user_upload_dir, exist_ok=True)
206
+
207
+ # Generate a safe filename to prevent path traversal
208
+ filename = os.path.basename(file.filename)
209
+ file_path = os.path.join(user_upload_dir, filename)
210
+
211
+ # Save the file
212
+ with open(file_path, "wb") as buffer:
213
+ shutil.copyfileobj(file.file, buffer)
214
+
215
+ # Return the file information
216
+ return {
217
+ "status": "ok",
218
+ "filename": filename,
219
+ "path": file_path,
220
+ "mime_type": file.content_type
221
+ }
222
+
@@ -235,6 +235,10 @@ async def send_message_to_agent(session_id: str, message: str | List[MessagePart
235
235
  img = dataurl_to_pil(part['data'])
236
236
  img_msg = await context.format_image_message(img)
237
237
  new_parts.append(img_msg)
238
+ elif part['type'] == 'text' and '[UPLOADED FILE]' in part['text']:
239
+ # Ensure we don't duplicate file entries
240
+ if not any('[UPLOADED FILE]' in p.get('text', '') for p in new_parts):
241
+ new_parts.append(part)
238
242
  else:
239
243
  new_parts.append(part)
240
244
  msg_to_add= {"role": "user", "content": new_parts }
@@ -248,6 +252,10 @@ async def send_message_to_agent(session_id: str, message: str | List[MessagePart
248
252
  results = []
249
253
  full_results = []
250
254
 
255
+ invalid = "ERROR, invalid response format."
256
+
257
+ consecutive_parse_errors = 0
258
+
251
259
  while continue_processing and iterations < max_iterations:
252
260
  iterations += 1
253
261
  continue_processing = False
@@ -256,7 +264,23 @@ async def send_message_to_agent(session_id: str, message: str | List[MessagePart
256
264
  if 'llm' in context.data:
257
265
  context.current_model = context.data['llm']
258
266
 
267
+ parse_error = False
268
+
259
269
  results, full_cmds = await agent_.chat_commands(context.current_model, context, messages=context.chat_log.get_recent())
270
+ for result in results:
271
+ if result['cmd'] == 'UNKNOWN':
272
+ consecutive_parse_errors += 1
273
+ parse_error = True
274
+
275
+ if not parse_error:
276
+ consecutive_parse_errors = 0
277
+
278
+ if consecutive_parse_errors > 6:
279
+ raise Exception("Too many consecutive parse errors, stopping processing.")
280
+
281
+ elif consecutive_parse_errors > 3:
282
+ results.append({"cmd": "UNKNOWN", "args": { "SYSTEM WARNING: Issue valid command list or task processing will be halted. Simplify output."}})
283
+
260
284
  try:
261
285
  tmp_data3 = { "results": full_cmds }
262
286
  tmp_data3 = await pipeline_manager.process_results(tmp_data3, context=context)
@@ -961,6 +961,41 @@ tbody tr {
961
961
  width: 32px;
962
962
  }
963
963
 
964
+ /* File attachment styles */
965
+ .file-attachment {
966
+ display: inline-flex;
967
+ align-items: center;
968
+ background: rgba(30, 30, 50, 0.6);
969
+ border-radius: 8px;
970
+ padding: 8px 12px;
971
+ margin: 5px 0;
972
+ max-width: 100%;
973
+ overflow: hidden;
974
+ }
975
+
976
+ .file-attachment a {
977
+ display: flex;
978
+ align-items: center;
979
+ color: #f0f0f0;
980
+ text-decoration: none;
981
+ overflow: hidden;
982
+ text-overflow: ellipsis;
983
+ white-space: nowrap;
984
+ }
985
+
986
+ .file-attachment svg {
987
+ margin-right: 8px;
988
+ flex-shrink: 0;
989
+ }
990
+
991
+ .file-attachment:hover {
992
+ background: rgba(40, 40, 70, 0.8);
993
+ }
994
+
995
+ .file-attachment a:hover {
996
+ text-decoration: underline;
997
+ }
998
+
964
999
  .left-side {
965
1000
  overflow-y: auto;
966
1001
  }
@@ -961,6 +961,41 @@ tbody tr {
961
961
  width: 32px;
962
962
  }
963
963
 
964
+ /* File attachment styles */
965
+ .file-attachment {
966
+ display: inline-flex;
967
+ align-items: center;
968
+ background: rgba(30, 30, 50, 0.6);
969
+ border-radius: 8px;
970
+ padding: 8px 12px;
971
+ margin: 5px 0;
972
+ max-width: 100%;
973
+ overflow: hidden;
974
+ }
975
+
976
+ .file-attachment a {
977
+ display: flex;
978
+ align-items: center;
979
+ color: #f0f0f0;
980
+ text-decoration: none;
981
+ overflow: hidden;
982
+ text-overflow: ellipsis;
983
+ white-space: nowrap;
984
+ }
985
+
986
+ .file-attachment svg {
987
+ margin-right: 8px;
988
+ flex-shrink: 0;
989
+ }
990
+
991
+ .file-attachment:hover {
992
+ background: rgba(40, 40, 70, 0.8);
993
+ }
994
+
995
+ .file-attachment a:hover {
996
+ text-decoration: underline;
997
+ }
998
+
964
999
  .left-side {
965
1000
  overflow-y: auto;
966
1001
  }
@@ -10,6 +10,7 @@ class ChatForm extends BaseEl {
10
10
  static properties = {
11
11
  sender: { type: String },
12
12
  message: { type: String },
13
+ uploadedFiles: { type: Array },
13
14
  taskid: { type: String },
14
15
  autoSizeInput: { type: Boolean, attribute: 'auto-size-input', reflect: true }
15
16
  }
@@ -109,6 +110,49 @@ class ChatForm extends BaseEl {
109
110
  font-size: 18px;
110
111
  padding: 0;
111
112
  }
113
+ .file-preview-container {
114
+ display: none;
115
+ margin-bottom: 10px;
116
+ }
117
+ .file-preview-container.has-files {
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ gap: 8px;
121
+ }
122
+ .file-item {
123
+ position: relative;
124
+ display: flex;
125
+ align-items: center;
126
+ background: rgba(30, 30, 50, 0.6);
127
+ border-radius: 4px;
128
+ padding: 8px 12px;
129
+ margin-bottom: 5px;
130
+ }
131
+ .file-icon {
132
+ margin-right: 8px;
133
+ }
134
+ .file-name {
135
+ flex-grow: 1;
136
+ white-space: nowrap;
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+ max-width: 200px;
140
+ }
141
+ .remove-file {
142
+ position: relative;
143
+ width: 24px;
144
+ height: 24px;
145
+ background: rgba(0, 0, 0, 0.3);
146
+ color: white;
147
+ border: none;
148
+ border-radius: 50%;
149
+ cursor: pointer;
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: center;
153
+ font-size: 18px;
154
+ padding: 0;
155
+ }
112
156
  .upload-container {
113
157
  display: flex;
114
158
  align-items: center;
@@ -123,12 +167,23 @@ class ChatForm extends BaseEl {
123
167
  cursor: pointer;
124
168
  color: inherit;
125
169
  }
170
+ .file-upload-button {
171
+ background: none;
172
+ border: none;
173
+ padding: 0;
174
+ margin-left: 8px;
175
+ cursor: pointer;
176
+ color: inherit;
177
+ }
126
178
  .upload-button:hover {
127
179
  opacity: 0.8;
128
180
  }
129
181
  #imageUpload {
130
182
  display: none;
131
183
  }
184
+ #fileUpload {
185
+ display: none;
186
+ }
132
187
  .loading {
133
188
  opacity: 0.5;
134
189
  pointer-events: none;
@@ -159,6 +214,7 @@ class ChatForm extends BaseEl {
159
214
  this.message = ''
160
215
  this.autoSizeInput = true
161
216
  this.taskid = null
217
+ this.uploadedFiles = []
162
218
  this.selectedImages = []
163
219
  this.isLoading = false
164
220
  }
@@ -181,6 +237,52 @@ class ChatForm extends BaseEl {
181
237
  }
182
238
  await this._processImage(file)
183
239
  }
240
+ console.log('File upload complete, file preview container:', this.shadowRoot.querySelector('.file-preview-container'))
241
+ }
242
+
243
+ async _handleFileUpload(e) {
244
+ const files = e.target.files
245
+ console.log('File upload triggered with files:', files)
246
+ if (!files || files.length === 0) return
247
+
248
+ this.isLoading = true
249
+ this.requestUpdate()
250
+
251
+ for (let file of files) {
252
+ try {
253
+ console.log('Processing file:', file.name, file.type, file.size)
254
+ const formData = new FormData()
255
+ formData.append('file', file)
256
+
257
+ const response = await authenticatedFetch(`/chat/${window.log_id}/upload`, {
258
+ method: 'POST',
259
+ body: formData
260
+ })
261
+
262
+ const result = await response.json()
263
+ console.log('Upload result:', result)
264
+ if (result.status === 'ok') {
265
+ if (this.uploadedFiles.some(f => f.filename === result.filename)) {
266
+ console.log('File already uploaded:', result.filename)
267
+ continue
268
+ }
269
+ this.uploadedFiles.push({
270
+ filename: result.filename,
271
+ path: result.path,
272
+ mime_type: result.mime_type
273
+ })
274
+ console.log('Updated uploadedFiles:', this.uploadedFiles)
275
+ }
276
+ } catch (error) {
277
+ console.error('Error uploading file:', error)
278
+ }
279
+ }
280
+
281
+ this._updateFilePreviews()
282
+ this.isLoading = false
283
+ this.requestUpdate()
284
+ e.target.value = '' // Reset file input
285
+ console.log('File upload complete, file preview container:', this.shadowRoot.querySelector('.file-preview-container'))
184
286
  }
185
287
 
186
288
  _resizeTextarea() {
@@ -249,6 +351,47 @@ class ChatForm extends BaseEl {
249
351
  }
250
352
  }
251
353
 
354
+ _updateFilePreviews() {
355
+ console.log('Updating file previews with files:', this.uploadedFiles)
356
+ const container = this.shadowRoot ? this.shadowRoot.querySelector('.file-preview-container') : null
357
+ if (!container) {
358
+ console.error('File preview container not found!')
359
+ return
360
+ } else {
361
+ container.style.display = 'block' // Make sure the container is visible
362
+ }
363
+
364
+ container.innerHTML = ''
365
+
366
+ if (this.uploadedFiles.length > 0) {
367
+ container.classList.add('has-files')
368
+ this.uploadedFiles.forEach((file, index) => {
369
+ console.log('Creating file item for:', file.filename)
370
+ const fileItem = document.createElement('div')
371
+ fileItem.className = 'file-item'
372
+ fileItem.innerHTML = `
373
+ <div class="file-icon">
374
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
375
+ <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
376
+ </svg>
377
+ </div>
378
+ <div class="file-name">${file.filename}</div>
379
+ <button class="remove-file" data-index="${index}">×</button>
380
+ `
381
+ fileItem.querySelector('.remove-file').addEventListener('click', () => this._removeFile(index))
382
+ container.appendChild(fileItem)
383
+ })
384
+ } else {
385
+ container.style.display = 'none' // Hide the container when empty
386
+ container.classList.remove('has-files')
387
+ }
388
+ }
389
+
390
+ _removeFile(index) {
391
+ this.uploadedFiles.splice(index, 1)
392
+ this._updateFilePreviews()
393
+ }
394
+
252
395
  _removeImage(index) {
253
396
  this.selectedImages.splice(index, 1)
254
397
  this._updateImagePreviews()
@@ -264,6 +407,24 @@ class ChatForm extends BaseEl {
264
407
 
265
408
  firstUpdated() {
266
409
  this.messageEl = this.shadowRoot.getElementById('inp_message');
410
+ this.fileUploadEl = this.shadowRoot.getElementById('fileUpload');
411
+
412
+ // Initialize file preview container
413
+ const filePreviewContainer = this.shadowRoot.querySelector('.file-preview-container');
414
+ if (!filePreviewContainer) {
415
+ console.error('File preview container not found in firstUpdated!');
416
+ } else {
417
+ console.log('File preview container initialized:', filePreviewContainer);
418
+ }
419
+
420
+ // Check if file upload input is properly initialized
421
+ if (!this.fileUploadEl) {
422
+ console.error('File upload input not found!');
423
+ } else {
424
+ console.log('File upload input initialized:', this.fileUploadEl);
425
+ this.fileUploadEl.addEventListener('change', this._handleFileUpload.bind(this));
426
+ }
427
+
267
428
  this.sendButton = this.shadowRoot.querySelector('.send_msg');
268
429
 
269
430
  const observer = new MutationObserver(() => {
@@ -308,6 +469,18 @@ class ChatForm extends BaseEl {
308
469
  })
309
470
  }
310
471
 
472
+ // Only add one file entry to avoid duplication
473
+ if (this.uploadedFiles.length > 0) {
474
+ const file = this.uploadedFiles[0];
475
+ console.log('Adding file to message:', file);
476
+
477
+ // Create a single file entry for all uploaded files
478
+ messageContent.push({
479
+ type: 'text',
480
+ text: `[UPLOADED FILE] Path: ${file.path}\nFilename: ${file.filename}\nType: ${file.mime_type}`
481
+ });
482
+ }
483
+
311
484
  for (let imageData of this.selectedImages) {
312
485
  messageContent.push({
313
486
  type: 'image',
@@ -327,7 +500,9 @@ class ChatForm extends BaseEl {
327
500
  this.dispatch('addmessage', ev_)
328
501
  this.messageEl.value = ''
329
502
  this.selectedImages = []
503
+ this.uploadedFiles = []
330
504
  this._updateImagePreviews()
505
+ this._updateFilePreviews()
331
506
  this._resizeTextarea()
332
507
  this.requestUpdate()
333
508
  }
@@ -338,6 +513,7 @@ class ChatForm extends BaseEl {
338
513
 
339
514
  <div class="chat-entry flex py-2 ${this.isLoading ? 'loading' : ''}">
340
515
  <div class="message-container">
516
+ <div class="file-preview-container"></div>
341
517
  <div class="image-preview-container"></div>
342
518
  <div class="upload-container">
343
519
  <label class="upload-button" title="Upload image">
@@ -348,6 +524,15 @@ class ChatForm extends BaseEl {
348
524
  <path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/>
349
525
  </svg>
350
526
  </label>
527
+ <label class="file-upload-button" title="Upload file">
528
+ <input type="file" id="fileUpload"
529
+ @change=${this._handleFileUpload}
530
+ multiple>
531
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
532
+ <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/>
533
+ <path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/>
534
+ </svg>
535
+ </label>
351
536
  </div>
352
537
  <textarea id="inp_message" class="message-input"
353
538
  @keydown=${(e) => {
@@ -0,0 +1,3 @@
1
+ # Import required modules for plugin to load properly
2
+ from .mod import *
3
+ from .router import router
@@ -0,0 +1,16 @@
1
+ {% block head %}
2
+ <link rel="stylesheet" href="/env_manager/static/css/env-manager.css">
3
+ <script type="module" src="/env_manager/static/js/env-manager.js"></script>
4
+ {% endblock %}
5
+
6
+ {% block content %}
7
+ <details>
8
+ <summary>
9
+ <span class="material-icons">settings_applications</span>
10
+ <span>Environment Variables</span>
11
+ </summary>
12
+ <div class="details-content">
13
+ <env-manager theme="dark" scope="local"></env-manager>
14
+ </div>
15
+ </details>
16
+ {% endblock %}