pygpt-net 2.6.67__py3-none-any.whl → 2.7.1__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.
Files changed (78) hide show
  1. pygpt_net/CHANGELOG.txt +20 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/assistant/assistant.py +13 -8
  4. pygpt_net/controller/assistant/batch.py +29 -15
  5. pygpt_net/controller/assistant/files.py +19 -14
  6. pygpt_net/controller/assistant/store.py +63 -41
  7. pygpt_net/controller/attachment/attachment.py +45 -35
  8. pygpt_net/controller/chat/attachment.py +50 -39
  9. pygpt_net/controller/config/field/dictionary.py +26 -14
  10. pygpt_net/controller/ctx/common.py +27 -17
  11. pygpt_net/controller/ctx/ctx.py +185 -101
  12. pygpt_net/controller/files/files.py +101 -41
  13. pygpt_net/controller/idx/indexer.py +87 -31
  14. pygpt_net/controller/kernel/kernel.py +13 -2
  15. pygpt_net/controller/mode/mode.py +3 -3
  16. pygpt_net/controller/model/editor.py +70 -15
  17. pygpt_net/controller/model/importer.py +153 -54
  18. pygpt_net/controller/painter/common.py +43 -11
  19. pygpt_net/controller/painter/painter.py +2 -2
  20. pygpt_net/controller/presets/experts.py +68 -15
  21. pygpt_net/controller/presets/presets.py +72 -36
  22. pygpt_net/controller/settings/profile.py +76 -35
  23. pygpt_net/controller/settings/workdir.py +70 -39
  24. pygpt_net/core/assistants/files.py +20 -18
  25. pygpt_net/core/filesystem/actions.py +111 -10
  26. pygpt_net/core/filesystem/filesystem.py +72 -1
  27. pygpt_net/core/filesystem/packer.py +161 -1
  28. pygpt_net/core/idx/idx.py +12 -11
  29. pygpt_net/core/idx/worker.py +13 -1
  30. pygpt_net/core/image/image.py +2 -2
  31. pygpt_net/core/models/models.py +4 -4
  32. pygpt_net/core/profile/profile.py +13 -3
  33. pygpt_net/core/video/video.py +2 -3
  34. pygpt_net/data/config/config.json +3 -3
  35. pygpt_net/data/config/models.json +3 -3
  36. pygpt_net/data/css/style.dark.css +45 -0
  37. pygpt_net/data/css/style.light.css +46 -0
  38. pygpt_net/data/locale/locale.de.ini +5 -1
  39. pygpt_net/data/locale/locale.en.ini +5 -1
  40. pygpt_net/data/locale/locale.es.ini +5 -1
  41. pygpt_net/data/locale/locale.fr.ini +5 -1
  42. pygpt_net/data/locale/locale.it.ini +5 -1
  43. pygpt_net/data/locale/locale.pl.ini +6 -2
  44. pygpt_net/data/locale/locale.uk.ini +5 -1
  45. pygpt_net/data/locale/locale.zh.ini +5 -1
  46. pygpt_net/provider/api/openai/__init__.py +4 -2
  47. pygpt_net/provider/core/config/patch.py +17 -1
  48. pygpt_net/tools/image_viewer/tool.py +17 -0
  49. pygpt_net/tools/text_editor/tool.py +9 -0
  50. pygpt_net/ui/__init__.py +2 -2
  51. pygpt_net/ui/dialog/preset.py +1 -0
  52. pygpt_net/ui/layout/ctx/ctx_list.py +16 -6
  53. pygpt_net/ui/layout/toolbox/image.py +2 -1
  54. pygpt_net/ui/layout/toolbox/indexes.py +2 -0
  55. pygpt_net/ui/layout/toolbox/video.py +5 -1
  56. pygpt_net/ui/main.py +3 -1
  57. pygpt_net/ui/widget/calendar/select.py +3 -3
  58. pygpt_net/ui/widget/draw/painter.py +238 -51
  59. pygpt_net/ui/widget/filesystem/explorer.py +1164 -142
  60. pygpt_net/ui/widget/lists/assistant.py +185 -24
  61. pygpt_net/ui/widget/lists/assistant_store.py +245 -42
  62. pygpt_net/ui/widget/lists/attachment.py +230 -47
  63. pygpt_net/ui/widget/lists/attachment_ctx.py +189 -33
  64. pygpt_net/ui/widget/lists/base_list_combo.py +2 -2
  65. pygpt_net/ui/widget/lists/context.py +1253 -70
  66. pygpt_net/ui/widget/lists/experts.py +110 -8
  67. pygpt_net/ui/widget/lists/model_editor.py +217 -14
  68. pygpt_net/ui/widget/lists/model_importer.py +125 -6
  69. pygpt_net/ui/widget/lists/preset.py +460 -71
  70. pygpt_net/ui/widget/lists/profile.py +149 -27
  71. pygpt_net/ui/widget/lists/uploaded.py +230 -38
  72. pygpt_net/ui/widget/option/combo.py +1211 -33
  73. pygpt_net/ui/widget/option/dictionary.py +35 -7
  74. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/METADATA +22 -57
  75. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/RECORD +78 -78
  76. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/LICENSE +0 -0
  77. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/WHEEL +0 -0
  78. {pygpt_net-2.6.67.dist-info → pygpt_net-2.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,11 +6,11 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 23:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
13
- from typing import List
13
+ from typing import List, Union
14
14
 
15
15
  from PySide6.QtGui import QAction, QIcon
16
16
  from PySide6.QtWidgets import QWidget
@@ -27,16 +27,22 @@ class Actions:
27
27
  """
28
28
  self.window = window
29
29
 
30
- def has_preview(self, path: str) -> bool:
30
+ def has_preview(self, path: Union[str, list]) -> bool:
31
31
  """
32
32
  Check if file has preview action
33
33
 
34
- :param path: path to file
34
+ :param path: path to file or list of paths
35
35
  :return: True if file has preview
36
36
  """
37
- if os.path.isdir(path):
37
+ if isinstance(path, list):
38
+ for p in path:
39
+ if not os.path.isdir(p):
40
+ return True # allow preview for any file in the list
38
41
  return False
39
- return True
42
+ else:
43
+ if os.path.isdir(path):
44
+ return False
45
+ return True
40
46
 
41
47
  def has_use(self, path: str) -> bool:
42
48
  """
@@ -48,14 +54,93 @@ class Actions:
48
54
  return (self.window.core.filesystem.types.is_image(path)
49
55
  or self.window.core.filesystem.types.is_video(path))
50
56
 
51
- def get_preview(self, parent: QWidget, path: str) -> List[QAction]:
57
+ def get_preview_batch(self, parent: QWidget, path: list) -> List[QAction]:
58
+ """
59
+ Get preview actions for multiple files
60
+
61
+ :param parent: explorer widget
62
+ :param path: list of paths
63
+ :return: list of context menu actions
64
+ """
65
+ actions = []
66
+ paths_video = []
67
+ paths_image = []
68
+ paths_edit = []
69
+ for p in path:
70
+ if os.path.isdir(p):
71
+ continue
72
+ actions = []
73
+ if (self.window.core.filesystem.types.is_video(p)
74
+ or self.window.core.filesystem.types.is_audio(p)):
75
+ paths_video.append(p)
76
+ elif self.window.core.filesystem.types.is_image(p):
77
+ paths_image.append(p)
78
+ else:
79
+ extra_excluded = ["pdf", "docx", "doc", "xlsx", "xls", "pptx", "ppt"]
80
+ ext = os.path.splitext(p)[1][1:].lower()
81
+ if ext not in self.window.core.filesystem.types.get_excluded_extensions() + extra_excluded:
82
+ paths_edit.append(p)
83
+
84
+ # video/audio - single action for first file
85
+ if paths_video:
86
+ p = paths_video[0] # single action for first video
87
+ action = QAction(
88
+ QIcon(":/icons/video.svg"),
89
+ trans('action.video.play'),
90
+ parent,
91
+ )
92
+ action.triggered.connect(
93
+ lambda: self.window.tools.get("player").play(p),
94
+ )
95
+ actions.append(action)
96
+ action = QAction(
97
+ QIcon(":/icons/hearing.svg"),
98
+ trans('action.video.transcribe'),
99
+ parent,
100
+ )
101
+ action.triggered.connect(
102
+ lambda: self.window.tools.get("transcriber").from_file(p),
103
+ )
104
+ actions.append(action)
105
+
106
+ # image - batch preview
107
+ if paths_image:
108
+ action = QAction(
109
+ QIcon(":/icons/image.svg"),
110
+ trans('action.preview'),
111
+ parent,
112
+ )
113
+ action.triggered.connect(
114
+ lambda: self.window.tools.get("viewer").open_preview_batch(paths_image),
115
+ )
116
+ actions.append(action)
117
+
118
+ # edit - batch edit
119
+ if paths_edit:
120
+ action = QAction(
121
+ QIcon(":/icons/edit.svg"),
122
+ trans('action.edit'),
123
+ parent,
124
+ )
125
+ action.triggered.connect(
126
+ lambda: self.window.tools.get("editor").open_batch(paths_edit),
127
+ )
128
+ actions.append(action)
129
+
130
+ return actions
131
+
132
+
133
+ def get_preview(self, parent: QWidget, path: Union[str, list]) -> List[QAction]:
52
134
  """
53
135
  Get preview actions for context menu
54
136
 
55
137
  :param parent: explorer widget
56
- :param path: path to file
138
+ :param path: path to file or list of paths
57
139
  :return: list of context menu actions
58
140
  """
141
+ if isinstance(path, list):
142
+ return self.get_preview_batch(parent, path)
143
+
59
144
  actions = []
60
145
  if (self.window.core.filesystem.types.is_video(path)
61
146
  or self.window.core.filesystem.types.is_audio(path)):
@@ -102,14 +187,30 @@ class Actions:
102
187
  actions.append(action)
103
188
  return actions
104
189
 
105
- def get_use(self, parent: QWidget, path: str) -> List[QAction]:
190
+ def get_use_batch(self, parent: QWidget, path: list) -> List[QAction]:
191
+ """
192
+ Get use actions for multiple files
193
+
194
+ :param parent: explorer widget
195
+ :param path: list of paths
196
+ :return: list of context menu actions
197
+ """
198
+ actions = []
199
+ for p in path:
200
+ actions.extend(self.get_use(parent, p))
201
+ return actions
202
+
203
+ def get_use(self, parent: QWidget, path: Union[str, list]) -> List[QAction]:
106
204
  """
107
205
  Get use actions for context menu
108
206
 
109
207
  :param parent: explorer widget
110
- :param path: path to file
208
+ :param path: path to file or list of paths
111
209
  :return: list of context menu actions
112
210
  """
211
+ if isinstance(path, list):
212
+ return self.get_use_batch(parent, path)
213
+
113
214
  actions = []
114
215
  if self.window.core.filesystem.types.is_image(path):
115
216
  action = QAction(
@@ -429,7 +429,8 @@ class Filesystem:
429
429
  os.remove(item_path)
430
430
  else:
431
431
  if item not in excluded_dirs:
432
- shutil.rmtree(item_path)
432
+ if os.path.exists(item_path) and os.path.isdir(item_path):
433
+ shutil.rmtree(item_path)
433
434
 
434
435
  return True
435
436
 
@@ -479,3 +480,73 @@ class Filesystem:
479
480
  else:
480
481
  files = [os.path.join(path, f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
481
482
  return files
483
+
484
+ # ===== Helpers for pack/unpack =====
485
+
486
+ def common_parent_dir(self, paths: List[str]) -> str:
487
+ """
488
+ Return a sensible common parent directory for the given paths.
489
+ For a single directory selection returns that directory; for a single file returns its parent.
490
+ For multiple selections returns a common existing parent, if resolvable, otherwise the parent of the first path.
491
+ """
492
+ if not paths:
493
+ return self.window.core.config.get_user_path()
494
+ norm = []
495
+ for p in paths:
496
+ p = os.path.abspath(p)
497
+ norm.append(p if os.path.isdir(p) else os.path.dirname(p))
498
+ if len(norm) == 1:
499
+ return norm[0]
500
+ try:
501
+ cp = os.path.commonpath(norm)
502
+ if os.path.isdir(cp):
503
+ return cp
504
+ return os.path.dirname(cp)
505
+ except Exception:
506
+ return norm[0]
507
+
508
+ def unique_path(self, directory: str, base_name: str, ext: str) -> str:
509
+ """
510
+ Return a unique file path within 'directory' for 'base_name' and 'ext' (ext should include dot or be empty).
511
+ Uses 'name', 'name (1)', 'name (2)', ... scheme.
512
+ """
513
+ os.makedirs(directory, exist_ok=True)
514
+ candidate = os.path.join(directory, f"{base_name}{ext}")
515
+ if not os.path.exists(candidate):
516
+ return candidate
517
+ i = 1
518
+ while True:
519
+ cand = os.path.join(directory, f"{base_name} ({i}){ext}")
520
+ if not os.path.exists(cand):
521
+ return cand
522
+ i += 1
523
+
524
+ def unique_dir(self, directory: str, base_name: str) -> str:
525
+ """
526
+ Return a unique directory path 'directory/base_name', adding ' (n)' suffix when needed.
527
+ """
528
+ os.makedirs(directory, exist_ok=True)
529
+ candidate = os.path.join(directory, base_name)
530
+ if not os.path.exists(candidate):
531
+ return candidate
532
+ i = 1
533
+ while True:
534
+ cand = os.path.join(directory, f"{base_name} ({i})")
535
+ if not os.path.exists(cand):
536
+ return cand
537
+ i += 1
538
+
539
+ def strip_archive_name(self, filename: str) -> str:
540
+ """
541
+ Strip known archive extensions (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz, .tgz, .tbz2, .txz) from a filename.
542
+ Returns filename without extension(s).
543
+ """
544
+ combos = ['.tar.gz', '.tar.bz2', '.tar.xz', '.tgz', '.tbz2', '.txz']
545
+ lower = filename.lower()
546
+ for suf in combos:
547
+ if lower.endswith(suf):
548
+ return filename[:-len(suf)]
549
+ root, ext = os.path.splitext(filename)
550
+ if ext.lower() in ('.zip', '.tar'):
551
+ return root
552
+ return root
@@ -84,4 +84,164 @@ class Packer:
84
84
  :param path: path to directory
85
85
  """
86
86
  if os.path.exists(path) and os.path.isdir(path):
87
- shutil.rmtree(path)
87
+ shutil.rmtree(path)
88
+
89
+ # ===== New high-level pack/unpack API (non-breaking) =====
90
+
91
+ def can_unpack(self, path: str) -> bool:
92
+ """
93
+ Check using stdlib detectors whether given file is a supported archive.
94
+ """
95
+ if not (path and os.path.isfile(path)):
96
+ return False
97
+ try:
98
+ import zipfile, tarfile
99
+ return zipfile.is_zipfile(path) or tarfile.is_tarfile(path)
100
+ except Exception:
101
+ return False
102
+
103
+ def _detect_kind(self, path: str) -> str:
104
+ """
105
+ Detect archive kind: 'zip' or 'tar'. Returns '' if unknown.
106
+ """
107
+ try:
108
+ import zipfile, tarfile
109
+ if zipfile.is_zipfile(path):
110
+ return 'zip'
111
+ if tarfile.is_tarfile(path):
112
+ return 'tar'
113
+ except Exception:
114
+ pass
115
+ return ''
116
+
117
+ def pack_paths(self, paths: list, fmt: str, dest_dir: str = None, base_name: str = None) -> str:
118
+ """
119
+ Pack given paths into a single archive.
120
+
121
+ :param paths: list of files/dirs to include
122
+ :param fmt: 'zip' or 'tar.gz'
123
+ :param dest_dir: output directory (default: common parent of paths)
124
+ :param base_name: output archive base name without extension (optional)
125
+ :return: created archive path or empty string on error
126
+ """
127
+ if not paths:
128
+ return ""
129
+ fs = self.window.core.filesystem
130
+ paths = [os.path.abspath(p) for p in paths if p]
131
+ dest_dir = dest_dir or fs.common_parent_dir(paths)
132
+
133
+ if base_name is None:
134
+ if len(paths) == 1:
135
+ name = os.path.basename(paths[0].rstrip(os.sep))
136
+ if os.path.isfile(paths[0]):
137
+ root, _ = os.path.splitext(name)
138
+ base_name = root or name
139
+ else:
140
+ base_name = name
141
+ else:
142
+ base_name = "archive"
143
+
144
+ fmt = (fmt or "").lower()
145
+ if fmt == 'zip':
146
+ ext = ".zip"
147
+ out_path = fs.unique_path(dest_dir, base_name, ext)
148
+ try:
149
+ self._pack_zip(paths, out_path)
150
+ return out_path
151
+ except Exception as e:
152
+ try:
153
+ self.window.core.debug.log(e)
154
+ except Exception:
155
+ pass
156
+ return ""
157
+ elif fmt in ('tar.gz', 'tgz'):
158
+ ext = ".tar.gz"
159
+ out_path = fs.unique_path(dest_dir, base_name, ext)
160
+ try:
161
+ self._pack_tar_gz(paths, out_path)
162
+ return out_path
163
+ except Exception as e:
164
+ try:
165
+ self.window.core.debug.log(e)
166
+ except Exception:
167
+ pass
168
+ return ""
169
+ else:
170
+ return ""
171
+
172
+ def _pack_zip(self, paths: list, out_path: str):
173
+ """
174
+ Create ZIP archive with selected paths, preserving top-level names.
175
+ """
176
+ import zipfile
177
+ with zipfile.ZipFile(out_path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
178
+ for src in paths:
179
+ top = os.path.basename(src.rstrip(os.sep))
180
+ if os.path.isdir(src):
181
+ for root, dirs, files in os.walk(src):
182
+ rel = os.path.relpath(root, src)
183
+ arc_root = top if rel == "." else os.path.join(top, rel)
184
+ # Add empty directories explicitly
185
+ if not files and not dirs:
186
+ zinfo = zipfile.ZipInfo(arc_root + "/")
187
+ zf.writestr(zinfo, "")
188
+ for f in files:
189
+ absf = os.path.join(root, f)
190
+ arcf = os.path.join(arc_root, f)
191
+ zf.write(absf, arcf)
192
+ else:
193
+ zf.write(src, top)
194
+
195
+ def _pack_tar_gz(self, paths: list, out_path: str):
196
+ """
197
+ Create TAR.GZ archive with selected paths, preserving top-level names.
198
+ """
199
+ import tarfile
200
+ with tarfile.open(out_path, 'w:gz') as tf:
201
+ for src in paths:
202
+ top = os.path.basename(src.rstrip(os.sep))
203
+ tf.add(src, arcname=top, recursive=True)
204
+
205
+ def unpack_to_dir(self, path: str, dest_dir: str) -> str:
206
+ """
207
+ Unpack archive at 'path' into 'dest_dir'.
208
+
209
+ :param path: archive file path
210
+ :param dest_dir: destination directory to extract into
211
+ :return: dest_dir on success, empty string otherwise
212
+ """
213
+ if not self.can_unpack(path):
214
+ return ""
215
+ try:
216
+ import zipfile, tarfile
217
+ os.makedirs(dest_dir, exist_ok=True)
218
+ kind = self._detect_kind(path)
219
+ if kind == 'zip':
220
+ with zipfile.ZipFile(path, 'r') as zf:
221
+ zf.extractall(dest_dir)
222
+ elif kind == 'tar':
223
+ with tarfile.open(path, 'r:*') as tf:
224
+ tf.extractall(dest_dir)
225
+ else:
226
+ return ""
227
+ return dest_dir
228
+ except Exception as e:
229
+ try:
230
+ self.window.core.debug.log(e)
231
+ except Exception:
232
+ pass
233
+ return ""
234
+
235
+ def unpack_to_sibling_dir(self, path: str) -> str:
236
+ """
237
+ Unpack archive into directory placed next to the archive, named after archive base name.
238
+
239
+ :param path: archive file path
240
+ :return: created directory path or empty string
241
+ """
242
+ if not (path and os.path.isfile(path)):
243
+ return ""
244
+ parent = os.path.dirname(path)
245
+ base = self.window.core.filesystem.strip_archive_name(os.path.basename(path))
246
+ out_dir = self.window.core.filesystem.unique_dir(parent, base)
247
+ return self.unpack_to_dir(path, out_dir)
pygpt_net/core/idx/idx.py CHANGED
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.21 07:00:00 #
9
+ # Updated Date: 2025.12.27 17:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
@@ -505,16 +505,17 @@ class Idx:
505
505
  self.llm.get_service_context(stream=False) # init environment only (ENV API keys, etc.)
506
506
  store_id = self.get_current_store()
507
507
  if store_id in self.items and idx in self.items[store_id]:
508
- if file in self.items[store_id][idx].items:
509
- # remove from storage
510
- doc_id = self.items[store_id][idx].items[file]["id"]
511
- self.storage.remove_document(
512
- id=idx,
513
- doc_id=doc_id,
514
- )
515
- # remove from index data and db
516
- del self.items[store_id][idx].items[file]
517
- self.files.remove(store_id, idx, doc_id)
508
+ for basename in list(self.items[store_id][idx].items.keys()):
509
+ item = self.items[store_id][idx].items[basename]
510
+ if file == item["path"]:
511
+ doc_id = item ["id"]
512
+ self.storage.remove_document(
513
+ id=idx,
514
+ doc_id=doc_id,
515
+ )
516
+ # remove from index data and db
517
+ del self.items[store_id][idx].items[basename]
518
+ self.files.remove(store_id, idx, doc_id)
518
519
 
519
520
  def load(self):
520
521
  """Load indexes and indexed items"""
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.21 07:00:00 #
9
+ # Updated Date: 2025.12.27 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from PySide6.QtCore import QObject, Signal, QRunnable, Slot
@@ -29,6 +29,7 @@ class IndexWorker(QRunnable):
29
29
  self.replace = None
30
30
  self.recursive = None
31
31
  self.from_ts = 0
32
+ self.from_ts_batch = {}
32
33
  self.idx = None
33
34
  self.type = None
34
35
  self.silent = False
@@ -70,6 +71,17 @@ class IndexWorker(QRunnable):
70
71
  self.content,
71
72
  self.from_ts,
72
73
  )
74
+ elif self.type == "db_meta_batch":
75
+ result = 0
76
+ errors = []
77
+ for meta_id in self.content:
78
+ r, e = self.window.core.idx.index_db_by_meta_id(
79
+ self.idx,
80
+ meta_id,
81
+ self.from_ts_batch[meta_id],
82
+ )
83
+ result += r
84
+ errors.extend(e)
73
85
  elif self.type == "db_current":
74
86
  result, errors = self.window.core.idx.index_db_from_updated_ts(
75
87
  self.idx,
@@ -152,7 +152,7 @@ class Image(QObject):
152
152
  """
153
153
  return {
154
154
  "type": "combo",
155
- "slider": True,
155
+ #"search": False,
156
156
  "label": "img_resolution",
157
157
  "value": "1024x1024",
158
158
  "keys": self.get_available_resolutions(),
@@ -166,7 +166,7 @@ class Image(QObject):
166
166
  """
167
167
  return {
168
168
  "type": "combo",
169
- "slider": True,
169
+ #"search": False,
170
170
  "label": "img_mode",
171
171
  "value": "image",
172
172
  "keys": self.get_available_modes(),
@@ -6,11 +6,11 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.09.17 19:00:00 #
9
+ # Updated Date: 2025.12.26 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
13
- from typing import Optional, List, Dict
13
+ from typing import Optional, List, Dict, Tuple
14
14
 
15
15
  from httpx_socks import SyncProxyTransport
16
16
  from openai import DefaultHttpxClient
@@ -272,7 +272,7 @@ class Models:
272
272
  """
273
273
  return self.multimodal
274
274
 
275
- def create_empty(self, append: bool = True) -> ModelItem:
275
+ def create_empty(self, append: bool = True) -> Tuple[ModelItem, str]:
276
276
  """
277
277
  Create new empty model
278
278
 
@@ -288,7 +288,7 @@ class Models:
288
288
  model.output = ["text"]
289
289
  if append:
290
290
  self.items[id] = model
291
- return model
291
+ return model, id
292
292
 
293
293
  def get_all(self) -> Dict[str, ModelItem]:
294
294
  """
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2024.12.14 22:00:00 #
9
+ # Updated Date: 2025.12.28 04:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -116,9 +116,19 @@ class Profile:
116
116
  'profiles': self.profiles
117
117
  }
118
118
  json_data = json.dumps(config, indent=4)
119
+ f_lock = os.path.join(self.base_workdir, self.PROFILE_FILE + '.lock')
119
120
  f = os.path.join(self.base_workdir, self.PROFILE_FILE)
120
- with open(f, 'w', encoding='utf-8') as f:
121
- f.write(json_data)
121
+ # check lock first
122
+ if os.path.exists(f_lock):
123
+ print("WARNING: Profile save aborted, lock exists. Please remove lock file:", f_lock)
124
+ return # abort if lock exists
125
+ with open(f_lock, 'w', encoding='utf-8') as file:
126
+ file.write('lock')
127
+ with open(f, 'w', encoding='utf-8') as file:
128
+ file.write(json_data)
129
+ # remove lock
130
+ if os.path.exists(f_lock):
131
+ os.remove(f_lock)
122
132
 
123
133
  def add(
124
134
  self,
@@ -276,7 +276,7 @@ class Video(QObject):
276
276
  """
277
277
  return {
278
278
  "type": "combo",
279
- "slider": True,
279
+ # "search": False,
280
280
  "label": "video.aspect_ratio",
281
281
  "value": "16:9",
282
282
  "keys": self.get_available_aspect_ratio(),
@@ -290,7 +290,7 @@ class Video(QObject):
290
290
  """
291
291
  return {
292
292
  "type": "combo",
293
- "slider": True,
293
+ # "search": False,
294
294
  "label": "video.resolution",
295
295
  "value": "720p",
296
296
  "keys": self.get_available_resolutions(),
@@ -304,7 +304,6 @@ class Video(QObject):
304
304
  """
305
305
  return {
306
306
  "type": "int",
307
- "slider": False,
308
307
  "label": "video.duration",
309
308
  "value": 8,
310
309
  "placeholder": "s",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.67",
4
- "app.version": "2.6.67",
5
- "updated_at": "2025-12-26T00:00:00"
3
+ "version": "2.7.1",
4
+ "app.version": "2.7.1",
5
+ "updated_at": "2025-12-28T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.67",
4
- "app.version": "2.6.67",
5
- "updated_at": "2025-12-26T00:00:00"
3
+ "version": "2.7.1",
4
+ "app.version": "2.7.1",
5
+ "updated_at": "2025-12-28T00:00:00"
6
6
  },
7
7
  "items": {
8
8
  "SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
@@ -174,4 +174,49 @@ NodeEditor {{
174
174
  /* Status Bar */
175
175
  #StatusBarTimer {{
176
176
  color: #999 !important;
177
+ }}
178
+
179
+ /* ComboBox */
180
+ QComboBox QAbstractItemView {{
181
+ border: 1px solid rgba(0,0,0,0.18);
182
+ }}
183
+
184
+ QListView#ComboPopupList {{
185
+ border-radius: 10px;
186
+ background: #1f2123;
187
+ border: 2px solid #4f5b62;
188
+ }}
189
+
190
+ QComboBox QAbstractItemView::item,
191
+ QListView#ComboPopupList::item {{
192
+ min-height: 20px;
193
+ padding: 8px 33px;
194
+ margin: 0;
195
+ color: #fff;
196
+ }}
197
+
198
+ QComboBox QAbstractItemView::item:selected,
199
+ QListView#ComboPopupList::item:selected {{
200
+ background: #fff;
201
+ color: #000;
202
+ selection-color: #000;
203
+ font-weight: bold;
204
+ }}
205
+
206
+ QListView#ComboPopupList::item:disabled {{
207
+ color: #c9c9c9;
208
+ background: #363d42;
209
+ padding: 15px;
210
+ }}
211
+
212
+ QComboBox QAbstractItemView::item:hover,
213
+ QListView#ComboPopupList::item:hover {{ }}
214
+
215
+ QWidget#ComboPopupWindow {{
216
+ background: transparent;
217
+ border: 0;
218
+ }}
219
+ QListView#ComboPopupList > QWidget#ComboPopupViewport {{
220
+ background: transparent;
221
+ border-radius: 0px;
177
222
  }}