pygpt-net 2.5.94__py3-none-any.whl → 2.5.95__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 (50) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +1 -1
  3. pygpt_net/controller/chat/text.py +3 -1
  4. pygpt_net/controller/dialogs/confirm.py +4 -1
  5. pygpt_net/controller/presets/editor.py +117 -1
  6. pygpt_net/controller/ui/mode.py +1 -3
  7. pygpt_net/controller/ui/tabs.py +58 -40
  8. pygpt_net/core/ctx/ctx.py +4 -2
  9. pygpt_net/core/presets/presets.py +12 -1
  10. pygpt_net/core/prompt/prompt.py +10 -1
  11. pygpt_net/core/render/web/body.py +14 -22
  12. pygpt_net/core/render/web/helpers.py +9 -2
  13. pygpt_net/core/render/web/renderer.py +44 -3
  14. pygpt_net/core/tabs/tabs.py +40 -5
  15. pygpt_net/core/text/text.py +31 -2
  16. pygpt_net/data/config/config.json +4 -2
  17. pygpt_net/data/config/models.json +2 -2
  18. pygpt_net/data/config/settings.json +28 -1
  19. pygpt_net/data/config/settings_section.json +3 -0
  20. pygpt_net/data/css/web-blocks.css +13 -2
  21. pygpt_net/data/css/web-blocks.dark.css +3 -0
  22. pygpt_net/data/css/web-blocks.light.css +3 -0
  23. pygpt_net/data/css/web-chatgpt.css +19 -2
  24. pygpt_net/data/css/web-chatgpt.dark.css +3 -0
  25. pygpt_net/data/css/web-chatgpt.light.css +3 -0
  26. pygpt_net/data/css/web-chatgpt_wide.css +19 -2
  27. pygpt_net/data/css/web-chatgpt_wide.dark.css +3 -0
  28. pygpt_net/data/css/web-chatgpt_wide.light.css +3 -0
  29. pygpt_net/data/locale/locale.de.ini +19 -0
  30. pygpt_net/data/locale/locale.en.ini +19 -0
  31. pygpt_net/data/locale/locale.es.ini +19 -0
  32. pygpt_net/data/locale/locale.fr.ini +19 -0
  33. pygpt_net/data/locale/locale.it.ini +19 -0
  34. pygpt_net/data/locale/locale.pl.ini +19 -0
  35. pygpt_net/data/locale/locale.uk.ini +19 -0
  36. pygpt_net/data/locale/locale.zh.ini +19 -0
  37. pygpt_net/item/preset.py +9 -1
  38. pygpt_net/provider/agents/openai/agent_b2b.py +17 -12
  39. pygpt_net/provider/core/config/patch.py +21 -1
  40. pygpt_net/provider/core/preset/json_file.py +6 -0
  41. pygpt_net/tools/translator/tool.py +9 -2
  42. pygpt_net/tools/translator/ui/widgets.py +45 -3
  43. pygpt_net/ui/base/config_dialog.py +1 -1
  44. pygpt_net/ui/dialog/preset.py +126 -8
  45. pygpt_net/ui/widget/textarea/search_input.py +68 -2
  46. {pygpt_net-2.5.94.dist-info → pygpt_net-2.5.95.dist-info}/METADATA +15 -2
  47. {pygpt_net-2.5.94.dist-info → pygpt_net-2.5.95.dist-info}/RECORD +50 -50
  48. {pygpt_net-2.5.94.dist-info → pygpt_net-2.5.95.dist-info}/LICENSE +0 -0
  49. {pygpt_net-2.5.94.dist-info → pygpt_net-2.5.95.dist-info}/WHEEL +0 -0
  50. {pygpt_net-2.5.94.dist-info → pygpt_net-2.5.95.dist-info}/entry_points.txt +0 -0
pygpt_net/CHANGELOG.txt CHANGED
@@ -1,3 +1,10 @@
1
+ 2.5.95 (2025-08-09)
2
+
3
+ - Added user info personalization in Config -> Personalization, where you can provide information about yourself to the model.
4
+ - Added presets personalization with configurable AI names and avatars.
5
+ - Added a search field in the Translator tool.
6
+ - Fixed <> tags replacement in code blocks.
7
+
1
8
  2.5.94 (2025-08-09)
2
9
 
3
10
  - Added a new LLM provider: HuggingFace Router.
pygpt_net/__init__.py CHANGED
@@ -13,7 +13,7 @@ __author__ = "Marcin Szczygliński"
13
13
  __copyright__ = "Copyright 2025, Marcin Szczygliński"
14
14
  __credits__ = ["Marcin Szczygliński"]
15
15
  __license__ = "MIT"
16
- __version__ = "2.5.94"
16
+ __version__ = "2.5.95"
17
17
  __build__ = "2025-08-09"
18
18
  __maintainer__ = "Marcin Szczygliński"
19
19
  __github__ = "https://github.com/szczyglis-dev/py-gpt"
@@ -102,8 +102,10 @@ class Text:
102
102
  stream_mode = False
103
103
 
104
104
  # create ctx item
105
+ meta = self.window.core.ctx.get_current_meta()
106
+ meta.preset = self.window.core.config.get('preset') # current preset
105
107
  ctx = CtxItem()
106
- ctx.meta = self.window.core.ctx.get_current_meta() # CtxMeta (owner object)
108
+ ctx.meta = meta # CtxMeta (owner object)
107
109
  ctx.internal = internal
108
110
  ctx.current = True # mark as current context item
109
111
  ctx.mode = mode # store current selected mode (not inline 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.08 05:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from typing import Any, Optional
@@ -94,6 +94,9 @@ class Confirm:
94
94
  elif type == 'translator.clear.right':
95
95
  self.window.tools.get("translator").clear_right(force=True)
96
96
 
97
+ elif type == "preset.avatar.delete":
98
+ self.window.controller.presets.editor.remove_avatar(True)
99
+
97
100
  # audio transcribe
98
101
  elif type == 'audio.transcribe':
99
102
  self.window.tools.get("transcriber").transcribe(id, force=True)
@@ -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.08.01 03:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import datetime
13
13
  import os
14
+ import shutil
14
15
  from typing import Any, Optional, Dict
15
16
 
16
17
  from PySide6.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout
@@ -48,6 +49,7 @@ class Editor:
48
49
  self.built = False
49
50
  self.tab_options_idx = {}
50
51
  self.opened = False
52
+ self.tmp_avatar = None
51
53
  self.options = {
52
54
  "filename": {
53
55
  "type": "text",
@@ -61,6 +63,15 @@ class Editor:
61
63
  "type": "text",
62
64
  "label": "preset.ai_name",
63
65
  },
66
+ "ai_avatar": {
67
+ "type": "text",
68
+ "label": "preset.ai_avatar",
69
+ },
70
+ "ai_personalize": {
71
+ "type": "bool",
72
+ "label": "preset.ai_personalize",
73
+ "description": "preset.ai_personalize.desc",
74
+ },
64
75
  "user_name": {
65
76
  "type": "text",
66
77
  "label": "preset.user_name",
@@ -716,6 +727,9 @@ class Editor:
716
727
  # update experts list, after ID loaded
717
728
  self.experts.update_list()
718
729
 
730
+ # setup avatar config
731
+ self.update_avatar_config(data)
732
+
719
733
  # restore functions
720
734
  if data.has_functions():
721
735
  functions = data.get_functions()
@@ -836,6 +850,14 @@ class Editor:
836
850
  # assign data from fields to preset object in items
837
851
  self.assign_data(id)
838
852
 
853
+ if is_new:
854
+ # assign tmp avatar
855
+ if self.tmp_avatar is not None:
856
+ self.window.core.presets.items[id].ai_avatar = self.tmp_avatar
857
+ self.tmp_avatar = None
858
+ else:
859
+ self.tmp_avatar = None
860
+
839
861
  # if agent, assign experts and select only agent mode
840
862
  curr_mode = self.window.core.config.get('mode')
841
863
  if curr_mode == MODE_AGENT:
@@ -956,6 +978,9 @@ class Editor:
956
978
  # extra options
957
979
  self.append_extra_options(preset)
958
980
 
981
+ # avatar update
982
+ self.update_avatar_config(preset)
983
+
959
984
  def to_current(self, preset: PresetItem):
960
985
  """
961
986
  Update preset field from editor
@@ -1018,3 +1043,94 @@ class Editor:
1018
1043
  preset.ai_name = value
1019
1044
  self.window.core.config.set('ai_name', preset.ai_name)
1020
1045
  self.window.core.presets.save(preset_id)
1046
+
1047
+ def upload_avatar(self, file_path: str):
1048
+ """
1049
+ Update avatar config for preset
1050
+
1051
+ :param file_path: path to the avatar file
1052
+ """
1053
+ preset = self.window.core.presets.get_by_uuid(self.current)
1054
+ presets_dir = self.window.core.config.get_user_dir("presets")
1055
+ avatars_dir = os.path.join(presets_dir, "avatars")
1056
+ preset_name = "_" if preset is None else preset.filename
1057
+ if not os.path.exists(avatars_dir):
1058
+ os.makedirs(avatars_dir, exist_ok=True)
1059
+ file_ext = os.path.splitext(file_path)[1]
1060
+ store_name = preset_name + "_" + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + file_ext
1061
+ avatar_path = os.path.join(avatars_dir, store_name)
1062
+
1063
+ # copy avatar to avatars directory
1064
+ if os.path.exists(avatar_path):
1065
+ os.remove(avatar_path)
1066
+ if os.path.exists(file_path):
1067
+ shutil.copy(file_path, avatar_path)
1068
+ if preset:
1069
+ preset.ai_avatar = store_name
1070
+ else:
1071
+ self.tmp_avatar = store_name
1072
+ self.window.controller.config.apply_value(
1073
+ parent_id=self.id,
1074
+ key="ai_avatar",
1075
+ option=self.options["ai_avatar"],
1076
+ value=store_name,
1077
+ )
1078
+ self.window.ui.nodes['preset.editor.avatar'].load_avatar(avatar_path)
1079
+ self.window.ui.nodes['preset.editor.avatar'].enable_remove_button(True)
1080
+ return avatar_path
1081
+
1082
+ def update_avatar_config(self, preset: PresetItem):
1083
+ """
1084
+ Update avatar config for preset
1085
+
1086
+ :param preset: preset item
1087
+ """
1088
+ avatar_path = preset.ai_avatar
1089
+ if avatar_path:
1090
+ file_path = os.path.join(
1091
+ self.window.core.config.get_user_dir("presets"),
1092
+ "avatars",
1093
+ avatar_path,
1094
+ )
1095
+ if not os.path.exists(file_path):
1096
+ self.window.ui.nodes['preset.editor.avatar'].remove_avatar()
1097
+ print("Avatar file does not exist:", file_path)
1098
+ return
1099
+ self.window.ui.nodes['preset.editor.avatar'].load_avatar(file_path)
1100
+ self.window.ui.nodes['preset.editor.avatar'].enable_remove_button(True)
1101
+ else:
1102
+ self.window.ui.nodes['preset.editor.avatar'].remove_avatar()
1103
+
1104
+ def remove_avatar(self, force: bool = False):
1105
+ """
1106
+ Remove avatar from preset editor
1107
+
1108
+ :param force: force remove avatar
1109
+ """
1110
+ if not force:
1111
+ self.window.ui.dialogs.confirm(
1112
+ type='preset.avatar.delete',
1113
+ id="",
1114
+ msg=trans('confirm.preset.avatar.delete'),
1115
+ )
1116
+ return
1117
+ preset = self.window.core.presets.get_by_uuid(self.current)
1118
+ if preset:
1119
+ current = preset.ai_avatar
1120
+ if current:
1121
+ presets_dir = self.window.core.config.get_user_dir("presets")
1122
+ avatars_dir = os.path.join(presets_dir, "avatars")
1123
+ avatar_path = os.path.join(avatars_dir, current)
1124
+ if os.path.exists(avatar_path):
1125
+ os.remove(avatar_path)
1126
+ preset.ai_avatar = ""
1127
+ else:
1128
+ self.tmp_avatar = None
1129
+
1130
+ self.window.ui.nodes['preset.editor.avatar'].remove_avatar()
1131
+ self.window.controller.config.apply_value(
1132
+ parent_id=self.id,
1133
+ key="ai_avatar",
1134
+ option=self.options["ai_avatar"],
1135
+ value="",
1136
+ )
@@ -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.01 03:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.types import (
@@ -82,10 +82,8 @@ class Mode:
82
82
  self.window.ui.nodes['preset.editor.remote_tools'].setVisible(False)
83
83
 
84
84
  if mode == MODE_COMPLETION:
85
- self.window.ui.nodes['preset.editor.ai_name'].setVisible(True)
86
85
  self.window.ui.nodes['preset.editor.user_name'].setVisible(True)
87
86
  else:
88
- self.window.ui.nodes['preset.editor.ai_name'].setVisible(False)
89
87
  self.window.ui.nodes['preset.editor.user_name'].setVisible(False)
90
88
 
91
89
  if mode == MODE_AGENT_OPENAI:
@@ -861,52 +861,70 @@ class Tabs:
861
861
  :param title: new tab name (optional, for chat tab)
862
862
  :param meta: context meta (optional, for chat tab)
863
863
  """
864
- # first try to focus current tab
864
+ # try to focus tab
865
865
  if self.get_current_type() != type:
866
- # first, check in second column
867
- second_column_idx = 1 if self.column_idx == 0 else 0
868
- # get current tab from second column
869
- tabs = self.window.ui.layout.get_tabs_by_idx(second_column_idx)
870
- second_tabs_idx = tabs.currentIndex()
871
- second_tab = self.window.core.tabs.get_tab_by_index(second_tabs_idx, second_column_idx)
872
- if second_tab is not None and second_tab.type == type:
873
- # switch to second column
874
- self.on_column_focus(second_column_idx)
875
- tabs.setCurrentIndex(second_tabs_idx)
876
- if meta:
877
- QTimer.singleShot(100, lambda: self.window.controller.ctx.load(meta.id))
878
- self.debug()
879
- return
880
866
 
881
- idx, column_idx, exists = self.window.core.tabs.get_min_idx_by_type_exists(type)
867
+ # find the closest tab in current column (on left side)
868
+ current = self.get_current_tab()
869
+ exists = False
870
+ if current:
871
+ idx, column_idx, exists = self.window.core.tabs.get_closest_idx_by_type_exists(
872
+ current,
873
+ type,
874
+ self.column_idx
875
+ )
882
876
  if exists:
883
- tabs = self.window.ui.layout.get_tabs_by_idx(column_idx)
884
- if tabs and idx:
885
- tabs.setCurrentIndex(idx)
886
- self.debug()
887
- return
877
+ tab = self.window.core.tabs.get_tab_by_index(idx, column_idx)
888
878
  else:
889
- # if current is not type, find first tab
879
+ # if not exists in current col, then find first idx in any column
890
880
  tab = self.window.core.tabs.get_first_by_type(type)
891
- if tab:
892
- tabs = self.window.ui.layout.get_tabs_by_idx(tab.column_idx)
893
- if tabs:
894
- idx = tab.idx
895
- if data_id is not None:
896
- tab.data_id = data_id
897
- if title is not None:
898
- self.update_title_current(title)
899
- else:
900
- self.on_column_focus(tab.column_idx)
901
- if meta is not None:
902
- self.on_column_focus(tab.column_idx)
903
- self.window.controller.ctx.load(meta.id)
904
- QTimer.singleShot(100, lambda: self.window.controller.ctx.load(meta.id))
905
- self.on_column_focus(tab.column_idx)
906
- tabs.setCurrentIndex(idx)
881
+
882
+ if tab:
883
+ # if tab is found in current column, switch to it
884
+ tabs = self.window.ui.layout.get_tabs_by_idx(tab.column_idx)
885
+ if tabs:
886
+ idx = tab.idx
887
+ if data_id is not None:
888
+ tab.data_id = data_id
889
+ if title is not None:
890
+ self.update_title_current(title)
891
+ else:
892
+ self.on_column_focus(tab.column_idx)
893
+ if meta is not None:
894
+ self.on_column_focus(tab.column_idx)
895
+ self.window.controller.ctx.load(meta.id)
896
+ QTimer.singleShot(100, lambda: self.window.controller.ctx.load(meta.id))
897
+ self.on_column_focus(tab.column_idx)
898
+ tabs.setCurrentIndex(idx)
899
+ else:
900
+ # if not found in current column, then check in second column
901
+ second_column_idx = 1 if self.column_idx == 0 else 0
902
+ # get current tab from second column
903
+ tabs = self.window.ui.layout.get_tabs_by_idx(second_column_idx)
904
+ second_tabs_idx = tabs.currentIndex()
905
+ second_tab = self.window.core.tabs.get_tab_by_index(second_tabs_idx, second_column_idx)
906
+ if second_tab is not None and second_tab.type == type:
907
+ # switch to second column
908
+ self.on_column_focus(second_column_idx)
909
+ tabs.setCurrentIndex(second_tabs_idx)
910
+ if meta:
911
+ QTimer.singleShot(100, lambda: self.window.controller.ctx.load(meta.id))
912
+
913
+ # if second and split screen disabled, then enable it
914
+ if tab and tab.column_idx == 1:
915
+ if not self.is_split_screen_enabled():
916
+ self.enable_split_screen(update_switch=True)
907
917
 
908
918
  self.debug()
909
919
 
920
+ def is_split_screen_enabled(self) -> bool:
921
+ """
922
+ Check if split screen mode is enabled
923
+
924
+ :return: True if split screen is enabled, False otherwise
925
+ """
926
+ return self.window.core.config.get("layout.split", False)
927
+
910
928
 
911
929
  def on_split_screen_changed(self, state: bool):
912
930
  """
@@ -914,7 +932,7 @@ class Tabs:
914
932
 
915
933
  :param state: True if split screen is enabled
916
934
  """
917
- prev_state = self.window.core.config.get("layout.split", False)
935
+ prev_state = self.is_split_screen_enabled()
918
936
  self.window.core.config.set("layout.split", state)
919
937
  if prev_state != state:
920
938
  if self.window.ui.nodes['layout.split'].box.isChecked() != state:
@@ -927,7 +945,7 @@ class Tabs:
927
945
 
928
946
  :param update_switch: True if switch should be updated
929
947
  """
930
- if self.window.core.config.get("layout.split", False):
948
+ if self.is_split_screen_enabled():
931
949
  return
932
950
 
933
951
  self.window.ui.splitters['columns'].setSizes([1, 1])
pygpt_net/core/ctx/ctx.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.07 02:00:00 #
9
+ # Updated Date: 2025.08.09 20:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -429,6 +429,8 @@ class Ctx:
429
429
  if meta is None:
430
430
  self.window.core.debug.log("Error creating new ctx")
431
431
  return
432
+ preset = self.window.core.config.get('preset')
433
+ meta.preset = preset
432
434
  self.meta[meta.id] = meta
433
435
  self.tmp_meta = meta
434
436
  self.current = meta.id
@@ -436,7 +438,7 @@ class Ctx:
436
438
  self.assistant = None
437
439
  self.mode = self.window.core.config.get('mode')
438
440
  self.model = self.window.core.config.get('model')
439
- self.preset = self.window.core.config.get('preset')
441
+ self.preset = preset
440
442
  self.clear_items()
441
443
  self.save(meta.id)
442
444
 
@@ -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.07.30 00:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import copy
@@ -295,6 +295,17 @@ class Presets:
295
295
  return presets[id]
296
296
  return None
297
297
 
298
+ def get(self, id: str) -> Optional[PresetItem]:
299
+ """
300
+ Return preset by ID
301
+
302
+ :param id: preset ID
303
+ :return: preset item
304
+ """
305
+ if id in self.items:
306
+ return self.items[id]
307
+ return None
308
+
298
309
  def get_by_uuid(self, uuid: str) -> Optional[PresetItem]:
299
310
  """
300
311
  Return preset by UUID
@@ -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.07.23 15:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  from pygpt_net.core.events import Event
@@ -149,6 +149,15 @@ class Prompt:
149
149
  self.window.dispatch(event)
150
150
  sys_prompt = event.data['value']
151
151
 
152
+ # append personalized about
153
+ about = self.window.core.config.get("personalize.about", "")
154
+ modes = self.window.core.config.get("personalize.modes", "")
155
+ if modes:
156
+ modes_list = modes.split(',')
157
+ if mode in modes_list:
158
+ if about:
159
+ sys_prompt += f"\n\n{about}\n\n"
160
+
152
161
  # event: post prompt (post-handle system prompt)
153
162
  event = Event(Event.POST_PROMPT, {
154
163
  'mode': mode,
@@ -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.08 19:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import os
@@ -497,15 +497,7 @@ class Body:
497
497
  hideTips();
498
498
  }
499
499
  function sanitize(content) {
500
- var parser = new DOMParser();
501
- var doc = parser.parseFromString(content, "text/html");
502
- var codeElements = doc.querySelectorAll('code, pre');
503
- codeElements.forEach(function(element) {
504
- var html = element.outerHTML;
505
- var newHtml = html.replace(/&amp;lt;/g, '<').replace(/&amp;gt;/g, '>');
506
- element.outerHTML = newHtml;
507
- });
508
- return doc.documentElement.outerHTML;
500
+ return content.replace(/&amp;lt;/g, '&lt;').replace(/&amp;gt;/g, '&gt;');
509
501
  }
510
502
  function highlightCode(withMath = true) {
511
503
  document.querySelectorAll('pre code').forEach(el => {
@@ -711,7 +703,7 @@ class Body:
711
703
  function endStream() {
712
704
  clearOutput();
713
705
  }
714
- function appendStream(bot_name, content, chunk, replace = false, is_code_block = false) {
706
+ function appendStream(name_header, content, chunk, replace = false, is_code_block = false) {
715
707
  hideTips();
716
708
  const element = getStreamContainer();
717
709
  doHighlight = true;
@@ -724,13 +716,13 @@ class Body:
724
716
  box = document.createElement('div');
725
717
  box.classList.add('msg-box');
726
718
  box.classList.add('msg-bot');
727
- const name = document.createElement('div');
728
- name.classList.add('name-header');
729
- name.classList.add('name-bot');
730
- name.textContent = bot_name;
719
+ if (name_header != '') {
720
+ const name = document.createElement('div');
721
+ name.innerHTML = name_header;
722
+ box.appendChild(name);
723
+ }
731
724
  msg = document.createElement('div');
732
725
  msg.classList.add('msg');
733
- box.appendChild(name);
734
726
  box.appendChild(msg);
735
727
  element.appendChild(box);
736
728
  } else {
@@ -799,7 +791,7 @@ class Body:
799
791
  scrollToBottom();
800
792
  }
801
793
  }
802
- function replaceOutput(bot_name, content) {
794
+ function replaceOutput(name_header, content) {
803
795
  hideTips();
804
796
  const element = getStreamContainer();
805
797
  if (element) {
@@ -809,13 +801,13 @@ class Body:
809
801
  box = document.createElement('div');
810
802
  box.classList.add('msg-box');
811
803
  box.classList.add('msg-bot');
812
- const name = document.createElement('div');
813
- name.classList.add('name-header');
814
- name.classList.add('name-bot');
815
- name.textContent = bot_name;
804
+ if (name_header != '') {
805
+ const name = document.createElement('div');
806
+ name.innerHTML = name_header;
807
+ box.appendChild(name);
808
+ }
816
809
  msg = document.createElement('div');
817
810
  msg.classList.add('msg');
818
- box.appendChild(name);
819
811
  box.appendChild(msg);
820
812
  element.appendChild(box);
821
813
  } else {
@@ -65,8 +65,15 @@ class Helpers:
65
65
  # replace HTML tags
66
66
  text = text.replace("<think>", "{{{{think}}}}")
67
67
  text = text.replace("</think>", "{{{{/think}}}}")
68
- text = text.replace("<", "&lt;")
69
- text = text.replace(">", "&gt;")
68
+ # text = text.replace("<", "&lt;")
69
+ # text = text.replace(">", "&gt;")
70
+ text = re.sub(
71
+ r'(\\\[.*?\\\])|(<)|(>)',
72
+ lambda m: m.group(1) if m.group(1)
73
+ else "&lt;" if m.group(2)
74
+ else "&gt;",
75
+ text, flags=re.DOTALL
76
+ ) # leave math formula tags
70
77
  text = text.replace("{{{{think}}}}", "<think>")
71
78
  text = text.replace("{{{{/think}}}}", "</think>")
72
79
  text = text.replace("<think>\n", "<think>")
@@ -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.01 19:00:00 #
9
+ # Updated Date: 2025.08.09 19:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -452,6 +452,7 @@ class Renderer(BaseRenderer):
452
452
  self.pids[pid].buffer = "" # always reset buffer
453
453
  return
454
454
 
455
+ name_header = self.get_name_header(ctx)
455
456
  self.update_names(meta, ctx)
456
457
  raw_chunk = str(text_chunk)
457
458
  raw_chunk = raw_chunk.replace("<", "&lt;")
@@ -521,6 +522,7 @@ class Renderer(BaseRenderer):
521
522
  raw_chunk = "\n" + raw_chunk # add newline to chunk
522
523
  escaped_chunk = json.dumps(raw_chunk)
523
524
  escaped_buffer = json.dumps(html)
525
+ name_header = json.dumps(name_header)
524
526
 
525
527
  if replace == "true":
526
528
  self.prev_chunk_replace = True
@@ -529,7 +531,7 @@ class Renderer(BaseRenderer):
529
531
 
530
532
  try:
531
533
  self.get_output_node(meta).page().runJavaScript(
532
- f"appendStream('{self.pids[pid].name_bot}', {escaped_buffer}, {escaped_chunk}, {replace}, {code_block_arg});")
534
+ f"appendStream({name_header}, {escaped_buffer}, {escaped_chunk}, {replace}, {code_block_arg});")
533
535
  except Exception as e:
534
536
  pass
535
537
 
@@ -1209,9 +1211,10 @@ class Renderer(BaseRenderer):
1209
1211
  if self.is_debug():
1210
1212
  debug = self.append_debug(ctx, pid, "output")
1211
1213
 
1214
+ name_header = self.get_name_header(ctx)
1212
1215
  html = (
1213
1216
  '<div class="msg-box msg-bot" id="{msg_id}">'
1214
- '<div class="name-header name-bot">{name_bot}</div>'
1217
+ + name_header +
1215
1218
  '<div class="msg">'
1216
1219
  '{html}'
1217
1220
  '<div class="msg-tool-extra">{tool_extra}</div>'
@@ -1234,6 +1237,44 @@ class Renderer(BaseRenderer):
1234
1237
 
1235
1238
  return html
1236
1239
 
1240
+ def get_name_header(self, ctx: CtxItem) -> str:
1241
+ """
1242
+ Get name header for the bot
1243
+
1244
+ :param ctx: CtxItem instance
1245
+ :return: HTML name header
1246
+ """
1247
+ meta = ctx.meta
1248
+ if meta is None:
1249
+ return ""
1250
+ preset_id = meta.preset
1251
+ if preset_id is None or preset_id == "":
1252
+ return ""
1253
+ preset = self.window.core.presets.get(preset_id)
1254
+ if preset is None:
1255
+ return ""
1256
+ if not preset.ai_personalize:
1257
+ return ""
1258
+
1259
+ output_name = ""
1260
+ avatar_html = ""
1261
+ if preset.ai_name:
1262
+ output_name = preset.ai_name
1263
+ if preset.ai_avatar:
1264
+ presets_dir = self.window.core.config.get_user_dir("presets")
1265
+ avatars_dir = os.path.join(presets_dir, "avatars")
1266
+ avatar_path = os.path.join(avatars_dir, preset.ai_avatar)
1267
+ if os.path.exists(avatar_path):
1268
+ if self.window.core.platforms.is_windows():
1269
+ prefix = 'file:///'
1270
+ else:
1271
+ prefix = 'file://'
1272
+ avatar_html = "<img src=\"" +prefix + avatar_path + "\" class=\"avatar\"> "
1273
+
1274
+ if not output_name and not avatar_html:
1275
+ return ""
1276
+ return "<div class=\"name-header name-bot\">" +avatar_html + output_name + "</div>"
1277
+
1237
1278
  def flush_output(
1238
1279
  self,
1239
1280
  pid: Optional[int],