pygpt-net 2.6.64__py3-none-any.whl → 2.6.66__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.
- pygpt_net/CHANGELOG.txt +21 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +5 -1
- pygpt_net/controller/chat/chat.py +0 -0
- pygpt_net/controller/chat/handler/openai_stream.py +137 -7
- pygpt_net/controller/chat/render.py +0 -0
- pygpt_net/controller/config/field/checkbox_list.py +34 -1
- pygpt_net/controller/files/files.py +71 -2
- pygpt_net/controller/media/media.py +20 -1
- pygpt_net/controller/presets/editor.py +137 -22
- pygpt_net/controller/presets/presets.py +4 -1
- pygpt_net/controller/ui/mode.py +14 -10
- pygpt_net/controller/ui/ui.py +18 -1
- pygpt_net/core/agents/custom/__init__.py +18 -2
- pygpt_net/core/agents/custom/runner.py +2 -2
- pygpt_net/core/attachments/clipboard.py +146 -0
- pygpt_net/core/image/image.py +34 -1
- pygpt_net/core/render/web/renderer.py +33 -11
- pygpt_net/core/tabs/tabs.py +0 -0
- pygpt_net/core/types/image.py +61 -3
- pygpt_net/data/config/config.json +4 -3
- pygpt_net/data/config/models.json +629 -41
- pygpt_net/data/css/style.dark.css +12 -0
- pygpt_net/data/css/style.light.css +12 -0
- pygpt_net/data/icons/pin2.svg +1 -0
- pygpt_net/data/icons/pin3.svg +3 -0
- pygpt_net/data/icons/point.svg +1 -0
- pygpt_net/data/icons/target.svg +1 -0
- pygpt_net/data/js/app/ui.js +19 -2
- pygpt_net/data/js/app/user.js +22 -54
- pygpt_net/data/js/app.min.js +7 -9
- pygpt_net/data/locale/locale.de.ini +4 -0
- pygpt_net/data/locale/locale.en.ini +8 -0
- pygpt_net/data/locale/locale.es.ini +4 -0
- pygpt_net/data/locale/locale.fr.ini +4 -0
- pygpt_net/data/locale/locale.it.ini +4 -0
- pygpt_net/data/locale/locale.pl.ini +4 -0
- pygpt_net/data/locale/locale.uk.ini +4 -0
- pygpt_net/data/locale/locale.zh.ini +4 -0
- pygpt_net/icons.qrc +4 -0
- pygpt_net/icons_rc.py +274 -137
- pygpt_net/item/model.py +15 -19
- pygpt_net/js_rc.py +2038 -2075
- pygpt_net/provider/agents/openai/agent.py +0 -0
- pygpt_net/provider/api/google/__init__.py +20 -9
- pygpt_net/provider/api/google/image.py +161 -28
- pygpt_net/provider/api/google/video.py +73 -36
- pygpt_net/provider/api/openai/__init__.py +21 -11
- pygpt_net/provider/api/openai/agents/client.py +0 -0
- pygpt_net/provider/api/openai/video.py +562 -0
- pygpt_net/provider/core/config/patch.py +15 -0
- pygpt_net/provider/core/model/patch.py +29 -3
- pygpt_net/provider/vector_stores/qdrant.py +117 -0
- pygpt_net/ui/__init__.py +6 -1
- pygpt_net/ui/dialog/preset.py +9 -4
- pygpt_net/ui/layout/chat/attachments.py +18 -1
- pygpt_net/ui/layout/status.py +3 -3
- pygpt_net/ui/layout/toolbox/raw.py +7 -1
- pygpt_net/ui/widget/element/status.py +55 -0
- pygpt_net/ui/widget/filesystem/explorer.py +116 -2
- pygpt_net/ui/widget/lists/context.py +26 -16
- pygpt_net/ui/widget/option/checkbox_list.py +14 -2
- pygpt_net/ui/widget/textarea/input.py +71 -17
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/METADATA +76 -25
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/RECORD +63 -55
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.64.dist-info → pygpt_net-2.6.66.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.30 13:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import os.path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from llama_index.core.indices.base import BaseIndex
|
|
17
|
+
from llama_index.core import StorageContext
|
|
18
|
+
|
|
19
|
+
from pygpt_net.utils import parse_args
|
|
20
|
+
from .base import BaseStore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QdrantProvider(BaseStore):
|
|
24
|
+
def __init__(self, *args, **kwargs):
|
|
25
|
+
super(QdrantProvider, self).__init__(*args, **kwargs)
|
|
26
|
+
"""
|
|
27
|
+
Qdrant vector store provider
|
|
28
|
+
|
|
29
|
+
:param args: args
|
|
30
|
+
:param kwargs: kwargs
|
|
31
|
+
"""
|
|
32
|
+
self.window = kwargs.get('window', None)
|
|
33
|
+
self.id = "QdrantVectorStore"
|
|
34
|
+
self.prefix = "qdrant_" # prefix for index directory
|
|
35
|
+
self.indexes = {}
|
|
36
|
+
|
|
37
|
+
def create(self, id: str):
|
|
38
|
+
"""
|
|
39
|
+
Create empty index
|
|
40
|
+
|
|
41
|
+
:param id: index name
|
|
42
|
+
"""
|
|
43
|
+
path = self.get_path(id)
|
|
44
|
+
if not os.path.exists(path):
|
|
45
|
+
os.makedirs(path, exist_ok=True)
|
|
46
|
+
self.store(id)
|
|
47
|
+
|
|
48
|
+
def get_qdrant_store(self, id: str):
|
|
49
|
+
"""
|
|
50
|
+
Get Qdrant vector store
|
|
51
|
+
|
|
52
|
+
:param id: index name
|
|
53
|
+
:return: QdrantVectorStore instance
|
|
54
|
+
"""
|
|
55
|
+
from llama_index.vector_stores.qdrant import QdrantVectorStore
|
|
56
|
+
|
|
57
|
+
additional_args = parse_args(
|
|
58
|
+
self.window.core.config.get('llama.idx.storage.args', []),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
url = additional_args.get('url', 'http://localhost:6333')
|
|
62
|
+
api_key = additional_args.get('api_key', '')
|
|
63
|
+
|
|
64
|
+
store_args = {k: v for k, v in additional_args.items() if k not in ['url', 'api_key', 'collection_name']}
|
|
65
|
+
|
|
66
|
+
return QdrantVectorStore(
|
|
67
|
+
url=url,
|
|
68
|
+
api_key=api_key,
|
|
69
|
+
collection_name=id,
|
|
70
|
+
**store_args
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def get(
|
|
74
|
+
self,
|
|
75
|
+
id: str,
|
|
76
|
+
llm: Optional = None,
|
|
77
|
+
embed_model: Optional = None,
|
|
78
|
+
) -> BaseIndex:
|
|
79
|
+
"""
|
|
80
|
+
Get index
|
|
81
|
+
|
|
82
|
+
:param id: index name
|
|
83
|
+
:param llm: LLM instance
|
|
84
|
+
:param embed_model: Embedding model instance
|
|
85
|
+
:return: index instance
|
|
86
|
+
"""
|
|
87
|
+
if not self.exists(id):
|
|
88
|
+
self.create(id)
|
|
89
|
+
vector_store = self.get_qdrant_store(id)
|
|
90
|
+
storage_context = StorageContext.from_defaults(
|
|
91
|
+
vector_store=vector_store,
|
|
92
|
+
)
|
|
93
|
+
self.indexes[id] = self.index_from_store(
|
|
94
|
+
vector_store=vector_store,
|
|
95
|
+
storage_context=storage_context,
|
|
96
|
+
llm=llm,
|
|
97
|
+
embed_model=embed_model,
|
|
98
|
+
)
|
|
99
|
+
return self.indexes[id]
|
|
100
|
+
|
|
101
|
+
def store(
|
|
102
|
+
self,
|
|
103
|
+
id: str,
|
|
104
|
+
index: Optional[BaseIndex] = None
|
|
105
|
+
):
|
|
106
|
+
"""
|
|
107
|
+
Store index
|
|
108
|
+
|
|
109
|
+
:param id: index name
|
|
110
|
+
:param index: index instance
|
|
111
|
+
"""
|
|
112
|
+
path = self.get_path(id)
|
|
113
|
+
os.makedirs(path, exist_ok=True)
|
|
114
|
+
lock_file = os.path.join(path, 'store.lock')
|
|
115
|
+
with open(lock_file, 'w') as f:
|
|
116
|
+
f.write(id + ': ' + str(datetime.datetime.now()))
|
|
117
|
+
self.indexes[id] = index
|
pygpt_net/ui/__init__.py
CHANGED
|
@@ -113,6 +113,8 @@ class UI:
|
|
|
113
113
|
"""Set default sizes"""
|
|
114
114
|
def set_initial_splitter_height():
|
|
115
115
|
"""Set initial splitter height"""
|
|
116
|
+
if 'main.output' not in self.window.ui.splitters:
|
|
117
|
+
return
|
|
116
118
|
total_height = self.window.ui.splitters['main.output'].size().height()
|
|
117
119
|
if total_height > 0:
|
|
118
120
|
size_output = int(total_height * 0.9)
|
|
@@ -124,6 +126,8 @@ class UI:
|
|
|
124
126
|
|
|
125
127
|
def set_initial_splitter_width():
|
|
126
128
|
"""Set initial splitter width"""
|
|
129
|
+
if 'main' not in self.window.ui.splitters:
|
|
130
|
+
return
|
|
127
131
|
total_width = self.window.ui.splitters['main'].size().width()
|
|
128
132
|
if total_width > 0:
|
|
129
133
|
size_output = int(total_width * 0.75)
|
|
@@ -139,7 +143,8 @@ class UI:
|
|
|
139
143
|
suffix = self.window.core.platforms.get_env_suffix()
|
|
140
144
|
profile_name = self.window.core.config.profile.get_current_name()
|
|
141
145
|
self.window.setWindowTitle(
|
|
142
|
-
f"PyGPT - Desktop AI Assistant {self.window.meta['version']} |
|
|
146
|
+
f"PyGPT - Desktop AI Assistant {self.window.meta['version']} | "
|
|
147
|
+
f"build {self.window.meta['build'].replace('.', '-')}{suffix} ({profile_name})"
|
|
143
148
|
)
|
|
144
149
|
|
|
145
150
|
def post_setup(self):
|
pygpt_net/ui/dialog/preset.py
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.28 08:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtCore import Qt
|
|
13
13
|
from PySide6.QtGui import QIcon
|
|
14
14
|
from PySide6.QtWidgets import QPushButton, QHBoxLayout, QLabel, QVBoxLayout, QSplitter, QWidget, QSizePolicy, \
|
|
15
|
-
QTabWidget, QFileDialog
|
|
15
|
+
QTabWidget, QFileDialog, QScrollArea, QFrame
|
|
16
16
|
|
|
17
17
|
from pygpt_net.core.types import (
|
|
18
18
|
MODE_AGENT,
|
|
@@ -169,7 +169,6 @@ class Preset(BaseConfigDialog):
|
|
|
169
169
|
desc.setContentsMargins(0, 5, 0, 5)
|
|
170
170
|
self.window.ui.nodes['preset.editor.description'] = desc
|
|
171
171
|
|
|
172
|
-
|
|
173
172
|
# prompt + extra options
|
|
174
173
|
prompt_layout = QVBoxLayout()
|
|
175
174
|
prompt_layout.addWidget(widgets['prompt'])
|
|
@@ -334,8 +333,14 @@ class Preset(BaseConfigDialog):
|
|
|
334
333
|
self.window.ui.nodes['preset.editor.extra'] = {}
|
|
335
334
|
|
|
336
335
|
tabs = QTabWidget()
|
|
336
|
+
|
|
337
|
+
# Make the prompt tab scrollable to avoid vertical overlap in narrow layouts.
|
|
338
|
+
scroll_prompt = QScrollArea()
|
|
339
|
+
scroll_prompt.setWidget(prompt_widget)
|
|
340
|
+
scroll_prompt.setWidgetResizable(True)
|
|
341
|
+
scroll_prompt.setFrameShape(QFrame.NoFrame)
|
|
337
342
|
tabs.addTab(
|
|
338
|
-
|
|
343
|
+
scroll_prompt,
|
|
339
344
|
trans("preset.prompt"),
|
|
340
345
|
)
|
|
341
346
|
tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
@@ -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.
|
|
9
|
+
# Updated Date: 2025.09.28 08:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -19,8 +19,10 @@ from PySide6.QtWidgets import QVBoxLayout, QPushButton, QHBoxLayout, QCheckBox,
|
|
|
19
19
|
from pygpt_net.item.attachment import AttachmentItem
|
|
20
20
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
21
21
|
from pygpt_net.ui.widget.lists.attachment import AttachmentList
|
|
22
|
+
from pygpt_net.core.attachments.clipboard import AttachmentDropHandler
|
|
22
23
|
from pygpt_net.utils import trans
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
class Attachments:
|
|
25
27
|
def __init__(self, window=None):
|
|
26
28
|
"""
|
|
@@ -30,6 +32,8 @@ class Attachments:
|
|
|
30
32
|
"""
|
|
31
33
|
self.window = window
|
|
32
34
|
self.id = 'attachments'
|
|
35
|
+
# Keep a strong reference to DnD handler(s)
|
|
36
|
+
self._dnd_handlers = {}
|
|
33
37
|
|
|
34
38
|
def setup(self) -> QVBoxLayout:
|
|
35
39
|
"""
|
|
@@ -132,6 +136,19 @@ class Attachments:
|
|
|
132
136
|
self.window.ui.models[self.id] = self.create_model(self.window)
|
|
133
137
|
self.window.ui.nodes[self.id].setModel(self.window.ui.models[self.id])
|
|
134
138
|
|
|
139
|
+
# Drag & Drop: allow dropping files/images/urls/text directly onto the list
|
|
140
|
+
try:
|
|
141
|
+
self._dnd_handlers[self.id] = AttachmentDropHandler(
|
|
142
|
+
self.window,
|
|
143
|
+
self.window.ui.nodes[self.id],
|
|
144
|
+
policy=AttachmentDropHandler.SWALLOW_ALL,
|
|
145
|
+
)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
try:
|
|
148
|
+
self.window.core.debug.log(e)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
135
152
|
def create_model(self, parent) -> QStandardItemModel:
|
|
136
153
|
"""
|
|
137
154
|
Create list model
|
pygpt_net/ui/layout/status.py
CHANGED
|
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import QLabel, QHBoxLayout, QSizePolicy, QPushButton
|
|
|
14
14
|
|
|
15
15
|
from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
16
16
|
from pygpt_net.ui.widget.anims.loader import Loader
|
|
17
|
+
from pygpt_net.ui.widget.element.status import BottomStatus
|
|
17
18
|
from pygpt_net.utils import trans
|
|
18
19
|
|
|
19
20
|
|
|
@@ -34,8 +35,7 @@ class Status:
|
|
|
34
35
|
"""
|
|
35
36
|
nodes = self.window.ui.nodes
|
|
36
37
|
|
|
37
|
-
nodes['status'] =
|
|
38
|
-
nodes['status'].setParent(self.window)
|
|
38
|
+
nodes['status'] = BottomStatus(window=self.window)
|
|
39
39
|
|
|
40
40
|
nodes['status.agent'] = HelpLabel("")
|
|
41
41
|
nodes['status.agent'].setParent(self.window)
|
|
@@ -53,7 +53,7 @@ class Status:
|
|
|
53
53
|
layout = QHBoxLayout()
|
|
54
54
|
layout.addWidget(nodes['anim.loading.status'])
|
|
55
55
|
layout.addWidget(nodes['status.agent'])
|
|
56
|
-
layout.addWidget(nodes['status'])
|
|
56
|
+
layout.addWidget(nodes['status'].setup())
|
|
57
57
|
layout.addWidget(nodes['global.stop'])
|
|
58
58
|
layout.setAlignment(Qt.AlignLeft)
|
|
59
59
|
|
|
@@ -6,11 +6,12 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.12.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QCheckBox
|
|
13
13
|
|
|
14
|
+
from pygpt_net.ui.widget.option.combo import OptionCombo
|
|
14
15
|
from pygpt_net.utils import trans
|
|
15
16
|
|
|
16
17
|
|
|
@@ -39,7 +40,12 @@ class Raw:
|
|
|
39
40
|
conf_global['img_raw'] = QCheckBox(trans("img.raw"), parent=container)
|
|
40
41
|
conf_global['img_raw'].toggled.connect(self.window.controller.media.toggle_raw)
|
|
41
42
|
|
|
43
|
+
conf_global = ui.config['global']
|
|
44
|
+
option_modes = self.window.core.image.get_available_modes()
|
|
45
|
+
conf_global['img_mode'] = OptionCombo(self.window, 'global', 'img_mode', option_modes)
|
|
46
|
+
|
|
42
47
|
cols = QHBoxLayout()
|
|
48
|
+
cols.addWidget(conf_global['img_mode'])
|
|
43
49
|
cols.addWidget(conf_global['img_raw'])
|
|
44
50
|
|
|
45
51
|
rows = QVBoxLayout()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.09.28 00:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from PySide6.QtWidgets import QLabel, QHBoxLayout, QWidget
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pygpt_net.utils import trans
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BottomStatus:
|
|
19
|
+
def __init__(self, window=None):
|
|
20
|
+
self.window = window
|
|
21
|
+
self.timer = QLabel(parent=self.window)
|
|
22
|
+
self.timer.setObjectName("StatusBarTimer")
|
|
23
|
+
self.msg = QLabel(parent=self.window)
|
|
24
|
+
self.msg.setObjectName("StatusBarMessage")
|
|
25
|
+
self.set_text(trans('status.started'))
|
|
26
|
+
|
|
27
|
+
def set_text(self, text):
|
|
28
|
+
"""Set status text"""
|
|
29
|
+
self.msg.setText(text)
|
|
30
|
+
if text:
|
|
31
|
+
now = datetime.now()
|
|
32
|
+
self.timer.setText(now.strftime("%H:%M"))
|
|
33
|
+
else:
|
|
34
|
+
self.timer.setText("")
|
|
35
|
+
|
|
36
|
+
def setText(self, text):
|
|
37
|
+
"""Fallback for set_text method"""
|
|
38
|
+
self.set_text(text)
|
|
39
|
+
|
|
40
|
+
def text(self) -> str:
|
|
41
|
+
"""Get status text"""
|
|
42
|
+
return self.msg.text()
|
|
43
|
+
|
|
44
|
+
def setup(self):
|
|
45
|
+
"""Setup status bar widget"""
|
|
46
|
+
self.timer.setText("00:00")
|
|
47
|
+
layout = QHBoxLayout()
|
|
48
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
49
|
+
layout.setSpacing(5)
|
|
50
|
+
layout.addWidget(self.timer)
|
|
51
|
+
layout.addWidget(self.msg)
|
|
52
|
+
layout.addStretch()
|
|
53
|
+
widget = QWidget(self.window)
|
|
54
|
+
widget.setLayout(layout)
|
|
55
|
+
return widget
|
|
@@ -6,13 +6,13 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.09.28 08:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
13
|
import os
|
|
14
14
|
|
|
15
|
-
from PySide6.QtCore import Qt, QModelIndex, QDir
|
|
15
|
+
from PySide6.QtCore import Qt, QModelIndex, QDir, QObject, QEvent
|
|
16
16
|
from PySide6.QtGui import QAction, QIcon, QCursor, QResizeEvent
|
|
17
17
|
from PySide6.QtWidgets import QTreeView, QMenu, QWidget, QVBoxLayout, QFileSystemModel, QLabel, QHBoxLayout, \
|
|
18
18
|
QPushButton, QSizePolicy
|
|
@@ -23,6 +23,117 @@ from pygpt_net.ui.widget.element.labels import HelpLabel
|
|
|
23
23
|
from pygpt_net.utils import trans
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class ExplorerDropHandler(QObject):
|
|
27
|
+
"""
|
|
28
|
+
Drag & drop handler for FileExplorer (uploads into target directory).
|
|
29
|
+
- Accepts local file and directory URLs.
|
|
30
|
+
- Determines target directory from drop position:
|
|
31
|
+
* directory item -> that directory
|
|
32
|
+
* file item -> parent directory
|
|
33
|
+
* empty area -> explorer root directory
|
|
34
|
+
- Uses Files controller to perform the actual copy and refresh view.
|
|
35
|
+
"""
|
|
36
|
+
def __init__(self, explorer):
|
|
37
|
+
super().__init__(explorer)
|
|
38
|
+
self.explorer = explorer
|
|
39
|
+
self.view = explorer.treeView
|
|
40
|
+
|
|
41
|
+
# Enable drops on both the view and its viewport
|
|
42
|
+
try:
|
|
43
|
+
self.view.setAcceptDrops(True)
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
vp = self.view.viewport()
|
|
47
|
+
if vp is not None:
|
|
48
|
+
try:
|
|
49
|
+
vp.setAcceptDrops(True)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
vp.installEventFilter(self)
|
|
53
|
+
self.view.installEventFilter(self)
|
|
54
|
+
|
|
55
|
+
def _mime_has_local_urls(self, md) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
if md and md.hasUrls():
|
|
58
|
+
for url in md.urls():
|
|
59
|
+
if url.isLocalFile():
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _local_paths_from_mime(self, md) -> list:
|
|
66
|
+
out = []
|
|
67
|
+
try:
|
|
68
|
+
if not (md and md.hasUrls()):
|
|
69
|
+
return out
|
|
70
|
+
for url in md.urls():
|
|
71
|
+
try:
|
|
72
|
+
if url.isLocalFile():
|
|
73
|
+
p = url.toLocalFile()
|
|
74
|
+
if p:
|
|
75
|
+
out.append(p)
|
|
76
|
+
except Exception:
|
|
77
|
+
continue
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
def _target_dir_from_pos(self, event) -> str:
|
|
83
|
+
# QDropEvent in Qt6: use position() -> QPointF
|
|
84
|
+
try:
|
|
85
|
+
pos = event.position().toPoint()
|
|
86
|
+
except Exception:
|
|
87
|
+
pos = event.pos()
|
|
88
|
+
idx = self.view.indexAt(pos)
|
|
89
|
+
if idx.isValid():
|
|
90
|
+
path = self.explorer.model.filePath(idx)
|
|
91
|
+
if os.path.isdir(path):
|
|
92
|
+
return path
|
|
93
|
+
return os.path.dirname(path)
|
|
94
|
+
# Fallback: explorer root directory
|
|
95
|
+
return self.explorer.directory
|
|
96
|
+
|
|
97
|
+
def eventFilter(self, obj, event):
|
|
98
|
+
et = event.type()
|
|
99
|
+
|
|
100
|
+
if et in (QEvent.DragEnter, QEvent.DragMove):
|
|
101
|
+
md = getattr(event, 'mimeData', lambda: None)()
|
|
102
|
+
if self._mime_has_local_urls(md):
|
|
103
|
+
try:
|
|
104
|
+
event.setDropAction(Qt.CopyAction)
|
|
105
|
+
event.acceptProposedAction()
|
|
106
|
+
except Exception:
|
|
107
|
+
event.accept()
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
if et == QEvent.Drop:
|
|
112
|
+
md = getattr(event, 'mimeData', lambda: None)()
|
|
113
|
+
if not self._mime_has_local_urls(md):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
paths = self._local_paths_from_mime(md)
|
|
117
|
+
target_dir = self._target_dir_from_pos(event)
|
|
118
|
+
try:
|
|
119
|
+
self.explorer.window.controller.files.upload_paths(paths, target_dir)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
try:
|
|
122
|
+
self.explorer.window.core.debug.log(e)
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
event.setDropAction(Qt.CopyAction)
|
|
128
|
+
event.acceptProposedAction()
|
|
129
|
+
except Exception:
|
|
130
|
+
event.accept()
|
|
131
|
+
# Swallow so the view does not try to handle the drop itself
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
26
137
|
class FileExplorer(QWidget):
|
|
27
138
|
def __init__(self, window, directory, index_data):
|
|
28
139
|
"""
|
|
@@ -138,6 +249,9 @@ class FileExplorer(QWidget):
|
|
|
138
249
|
'db': QIcon(":/icons/db.svg"),
|
|
139
250
|
}
|
|
140
251
|
|
|
252
|
+
# Drag & Drop upload support
|
|
253
|
+
self._dnd_handler = ExplorerDropHandler(self)
|
|
254
|
+
|
|
141
255
|
def eventFilter(self, source, event):
|
|
142
256
|
"""
|
|
143
257
|
Focus event filter
|
|
@@ -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.09.
|
|
9
|
+
# Updated Date: 2025.09.28 00:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
@@ -40,7 +40,7 @@ class ContextList(BaseList):
|
|
|
40
40
|
'chat': QIcon(":/icons/chat.svg"),
|
|
41
41
|
'copy': QIcon(":/icons/copy.svg"),
|
|
42
42
|
'close': QIcon(":/icons/close.svg"),
|
|
43
|
-
'pin': QIcon(":/icons/
|
|
43
|
+
'pin': QIcon(":/icons/pin3.svg"),
|
|
44
44
|
'clock': QIcon(":/icons/clock.svg"),
|
|
45
45
|
'db': QIcon(":/icons/db.svg"),
|
|
46
46
|
'folder': QIcon(":/icons/folder_filled.svg"),
|
|
@@ -49,7 +49,8 @@ class ContextList(BaseList):
|
|
|
49
49
|
self._color_icon_cache = {}
|
|
50
50
|
|
|
51
51
|
# Use a custom delegate for labels/pinned/attachment indicators and group border indicator
|
|
52
|
-
|
|
52
|
+
# Pass both: attachment icon and pin icon (pin2.svg) for pinned indicator rendering
|
|
53
|
+
self.setItemDelegate(ImportantItemDelegate(self, self._icons['attachment'], self._icons['pin']))
|
|
53
54
|
|
|
54
55
|
# Ensure context menu works as before
|
|
55
56
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
@@ -467,15 +468,17 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
|
|
|
467
468
|
"""
|
|
468
469
|
Item delegate that paints:
|
|
469
470
|
- Attachment icon on the right side (centered vertically),
|
|
470
|
-
- Pinned indicator (
|
|
471
|
+
- Pinned indicator (pin.svg icon) in the top-right corner (overlays if needed),
|
|
471
472
|
- Label color as a full-height vertical bar on the left for labeled items,
|
|
472
473
|
- Group enclosure indicator for expanded groups:
|
|
473
474
|
- thin vertical bar (default 2 px) on the left side of child rows area,
|
|
474
475
|
- thin horizontal bar (default 2 px) at the bottom of the last child row.
|
|
475
476
|
"""
|
|
476
|
-
def __init__(self, parent=None, attachment_icon: QIcon = None):
|
|
477
|
+
def __init__(self, parent=None, attachment_icon: QIcon = None, pin_icon: QIcon = None):
|
|
477
478
|
super().__init__(parent)
|
|
478
479
|
self._attachment_icon = attachment_icon or QIcon(":/icons/attachment.svg")
|
|
480
|
+
# Use provided pin icon (transparent background) as pinned indicator
|
|
481
|
+
self._pin_icon = pin_icon or QIcon(":/icons/pin.svg")
|
|
479
482
|
|
|
480
483
|
# Predefined label colors (status -> QColor)
|
|
481
484
|
self._status_colors = {
|
|
@@ -490,8 +493,8 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
|
|
|
490
493
|
}
|
|
491
494
|
|
|
492
495
|
# Visual tuning constants
|
|
493
|
-
self._pin_pen = QtGui.QPen(QtCore.Qt.black, 0.5, QtCore.Qt.SolidLine)
|
|
494
|
-
self._pin_diameter = 4 #
|
|
496
|
+
self._pin_pen = QtGui.QPen(QtCore.Qt.black, 0.5, QtCore.Qt.SolidLine) # kept for compatibility
|
|
497
|
+
self._pin_diameter = 4 # legacy circle diameter (not used anymore)
|
|
495
498
|
self._pin_margin = 3 # Margin from top and right edges
|
|
496
499
|
self._attach_spacing = 4 # Kept for potential future layout tweaks
|
|
497
500
|
self._label_bar_width = 4 # Full-height label bar width (left side)
|
|
@@ -507,6 +510,10 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
|
|
|
507
510
|
self._group_indicator_gap = 6 # gap between child content left and the vertical bar
|
|
508
511
|
self._group_indicator_bottom_offset = 6
|
|
509
512
|
|
|
513
|
+
# Pinned icon sizing (kept deliberately small, similar to previous yellow dot)
|
|
514
|
+
# The actual painted size is min(max_size, availableHeightWithMargins)
|
|
515
|
+
self._pin_icon_max_size = 12 # px
|
|
516
|
+
|
|
510
517
|
# Try to load customization from application config (safe if missing)
|
|
511
518
|
self._init_group_indicator_from_config()
|
|
512
519
|
|
|
@@ -665,20 +672,23 @@ class ImportantItemDelegate(QtWidgets.QStyledItemDelegate):
|
|
|
665
672
|
)
|
|
666
673
|
self._attachment_icon.paint(painter, icon_rect, QtCore.Qt.AlignCenter)
|
|
667
674
|
|
|
668
|
-
# Pinned indicator
|
|
669
|
-
# It
|
|
675
|
+
# Pinned indicator: small pin.svg painted at fixed top-right position.
|
|
676
|
+
# It overlays above any other right-side icons.
|
|
670
677
|
if is_important:
|
|
671
678
|
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
|
|
672
679
|
painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver)
|
|
673
|
-
color = self.get_color_for_status(3)
|
|
674
680
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
681
|
+
# Compute a compact size similar in footprint to previous circle,
|
|
682
|
+
# but readable for vector icon; clamp to available height.
|
|
683
|
+
available = max(8, option.rect.height() - 2 * self._pin_margin)
|
|
684
|
+
pin_size = min(self._pin_icon_max_size, available)
|
|
678
685
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
686
|
+
x = option.rect.right() - self._pin_margin - pin_size
|
|
687
|
+
y = option.rect.top() + self._pin_margin
|
|
688
|
+
pin_rect = QtCore.QRect(x, y, pin_size, pin_size)
|
|
689
|
+
|
|
690
|
+
# Paint the pin icon (transparent background)
|
|
691
|
+
self._pin_icon.paint(painter, pin_rect, QtCore.Qt.AlignCenter)
|
|
682
692
|
|
|
683
693
|
# Label bar on the left with 3px vertical margins
|
|
684
694
|
if label > 0:
|
|
@@ -6,10 +6,10 @@
|
|
|
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.
|
|
9
|
+
# Updated Date: 2025.12.25 20:00:00 #
|
|
10
10
|
# ================================================== #
|
|
11
11
|
|
|
12
|
-
from PySide6.QtWidgets import QCheckBox, QWidget
|
|
12
|
+
from PySide6.QtWidgets import QCheckBox, QWidget, QPushButton
|
|
13
13
|
|
|
14
14
|
from pygpt_net.ui.base.flow_layout import FlowLayout
|
|
15
15
|
from pygpt_net.utils import trans
|
|
@@ -84,6 +84,18 @@ class OptionCheckboxList(QWidget):
|
|
|
84
84
|
for widget in widgets:
|
|
85
85
|
self.layout.addWidget(widget)
|
|
86
86
|
|
|
87
|
+
# select/unselect all button
|
|
88
|
+
btn_select = QPushButton("X", self)
|
|
89
|
+
btn_select.setToolTip(trans("action.select_unselect_all"))
|
|
90
|
+
btn_select.clicked.connect(
|
|
91
|
+
lambda: self.window.controller.config.checkbox_list.on_select_all(
|
|
92
|
+
self.parent_id,
|
|
93
|
+
self.id,
|
|
94
|
+
self.option
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
self.layout.addWidget(btn_select)
|
|
98
|
+
|
|
87
99
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
88
100
|
self.setLayout(self.layout)
|
|
89
101
|
|