pygpt-net 2.6.3__py3-none-any.whl → 2.6.4__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 (66) hide show
  1. pygpt_net/CHANGELOG.txt +5 -0
  2. pygpt_net/__init__.py +1 -1
  3. pygpt_net/config.py +55 -65
  4. pygpt_net/controller/chat/chat.py +38 -35
  5. pygpt_net/controller/chat/render.py +144 -217
  6. pygpt_net/controller/chat/stream.py +51 -25
  7. pygpt_net/controller/config/config.py +39 -42
  8. pygpt_net/controller/config/field/checkbox.py +16 -12
  9. pygpt_net/controller/config/field/checkbox_list.py +36 -31
  10. pygpt_net/controller/config/field/cmd.py +51 -57
  11. pygpt_net/controller/config/field/combo.py +33 -16
  12. pygpt_net/controller/config/field/dictionary.py +48 -55
  13. pygpt_net/controller/config/field/input.py +50 -32
  14. pygpt_net/controller/config/field/slider.py +40 -45
  15. pygpt_net/controller/config/field/textarea.py +20 -6
  16. pygpt_net/controller/config/placeholder.py +110 -231
  17. pygpt_net/controller/lang/mapping.py +57 -95
  18. pygpt_net/controller/lang/plugins.py +64 -55
  19. pygpt_net/controller/lang/settings.py +39 -38
  20. pygpt_net/controller/layout/layout.py +11 -2
  21. pygpt_net/controller/plugins/plugins.py +19 -1
  22. pygpt_net/controller/ui/mode.py +107 -125
  23. pygpt_net/core/bridge/bridge.py +5 -5
  24. pygpt_net/core/command/command.py +149 -219
  25. pygpt_net/core/ctx/ctx.py +94 -146
  26. pygpt_net/core/debug/debug.py +48 -58
  27. pygpt_net/core/models/models.py +74 -112
  28. pygpt_net/core/modes/modes.py +13 -21
  29. pygpt_net/core/plugins/plugins.py +154 -177
  30. pygpt_net/core/presets/presets.py +103 -176
  31. pygpt_net/core/render/web/body.py +2 -3
  32. pygpt_net/core/render/web/renderer.py +109 -180
  33. pygpt_net/core/text/utils.py +28 -44
  34. pygpt_net/core/tokens/tokens.py +104 -203
  35. pygpt_net/data/config/config.json +2 -2
  36. pygpt_net/data/config/models.json +2 -2
  37. pygpt_net/item/ctx.py +141 -139
  38. pygpt_net/plugin/agent/plugin.py +2 -1
  39. pygpt_net/plugin/audio_output/plugin.py +5 -2
  40. pygpt_net/plugin/base/plugin.py +77 -93
  41. pygpt_net/plugin/bitbucket/plugin.py +3 -2
  42. pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
  43. pygpt_net/plugin/cmd_custom/plugin.py +3 -2
  44. pygpt_net/plugin/cmd_files/plugin.py +3 -2
  45. pygpt_net/plugin/cmd_history/plugin.py +3 -2
  46. pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
  47. pygpt_net/plugin/cmd_serial/plugin.py +3 -2
  48. pygpt_net/plugin/cmd_system/plugin.py +3 -6
  49. pygpt_net/plugin/cmd_web/plugin.py +3 -2
  50. pygpt_net/plugin/experts/plugin.py +2 -2
  51. pygpt_net/plugin/facebook/plugin.py +3 -4
  52. pygpt_net/plugin/github/plugin.py +4 -2
  53. pygpt_net/plugin/google/plugin.py +3 -3
  54. pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
  55. pygpt_net/plugin/mailer/plugin.py +3 -5
  56. pygpt_net/plugin/openai_vision/plugin.py +3 -2
  57. pygpt_net/plugin/real_time/plugin.py +52 -60
  58. pygpt_net/plugin/slack/plugin.py +3 -4
  59. pygpt_net/plugin/telegram/plugin.py +3 -4
  60. pygpt_net/plugin/twitter/plugin.py +3 -4
  61. pygpt_net/ui/widget/textarea/web.py +18 -14
  62. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.4.dist-info}/METADATA +7 -2
  63. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.4.dist-info}/RECORD +66 -66
  64. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.4.dist-info}/LICENSE +0 -0
  65. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.4.dist-info}/WHEEL +0 -0
  66. {pygpt_net-2.6.3.dist-info → pygpt_net-2.6.4.dist-info}/entry_points.txt +0 -0
@@ -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.15 03:00:00 #
9
+ # Updated Date: 2025.08.15 23:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
@@ -15,8 +15,6 @@ import re
15
15
  from datetime import datetime
16
16
  from typing import Optional, List
17
17
 
18
- from PySide6 import QtCore
19
-
20
18
  from pygpt_net.core.render.base import BaseRenderer
21
19
  from pygpt_net.core.text.utils import has_unclosed_code_tag
22
20
  from pygpt_net.item.ctx import CtxItem, CtxMeta
@@ -51,6 +49,11 @@ class Renderer(BaseRenderer):
51
49
  self.prev_chunk_replace = False
52
50
  self.prev_chunk_newline = False
53
51
 
52
+ app_path = self.window.core.config.get_app_path() if self.window else ""
53
+ self._icon_expand = os.path.join(app_path, "data", "icons", "expand.svg")
54
+ self._icon_sync = os.path.join(app_path, "data", "icons", "sync.svg")
55
+ self._file_prefix = 'file:///' if self.window and self.window.core.platforms.is_windows() else 'file://'
56
+
54
57
  def prepare(self):
55
58
  """
56
59
  Prepare renderer
@@ -83,7 +86,7 @@ class Renderer(BaseRenderer):
83
86
  :param meta: context meta
84
87
  :param tab: Tab
85
88
  """
86
- if meta is None:
89
+ if meta is None or tab is None:
87
90
  return
88
91
  pid = tab.pid
89
92
  if pid is None or pid not in self.pids:
@@ -157,7 +160,6 @@ class Renderer(BaseRenderer):
157
160
  :param state: state name
158
161
  :param meta: context meta
159
162
  """
160
- # BUSY: current pid only
161
163
  if state == RenderEvent.STATE_BUSY:
162
164
  if meta:
163
165
  pid = self.get_pid(meta)
@@ -169,7 +171,6 @@ class Renderer(BaseRenderer):
169
171
  except Exception as e:
170
172
  pass
171
173
 
172
- # IDLE: all pids
173
174
  elif state == RenderEvent.STATE_IDLE:
174
175
  for pid in self.pids:
175
176
  node = self.get_output_node_by_pid(pid)
@@ -180,7 +181,6 @@ class Renderer(BaseRenderer):
180
181
  except Exception as e:
181
182
  pass
182
183
 
183
- # ERROR: all pids
184
184
  elif state == RenderEvent.STATE_ERROR:
185
185
  for pid in self.pids:
186
186
  node = self.get_output_node_by_pid(pid)
@@ -207,7 +207,7 @@ class Renderer(BaseRenderer):
207
207
  pid = self.get_or_create_pid(meta)
208
208
  self.init(pid)
209
209
  self.reset_names(meta)
210
- self.tool_output_end() # reset tools
210
+ self.tool_output_end()
211
211
  self.prev_chunk_replace = False
212
212
 
213
213
  def end(
@@ -229,6 +229,7 @@ class Renderer(BaseRenderer):
229
229
  if self.pids[pid].item is not None and stream:
230
230
  self.append_context_item(meta, self.pids[pid].item)
231
231
  self.pids[pid].item = None
232
+ self.pids[pid].clear()
232
233
 
233
234
  def end_extra(
234
235
  self,
@@ -309,34 +310,32 @@ class Renderer(BaseRenderer):
309
310
 
310
311
  if clear:
311
312
  self.reset(meta)
312
- i = 0
313
313
 
314
314
  self.pids[pid].use_buffer = True
315
315
  self.pids[pid].html = ""
316
316
  prev_ctx = None
317
- for item in items:
317
+ total = len(items)
318
+ for i, item in enumerate(items):
318
319
  self.update_names(meta, item)
319
320
  item.idx = i
320
321
  if i == 0:
321
322
  item.first = True
322
- next_item = items[i + 1] if i + 1 < len(items) else None # append next item if exists
323
+ next_item = items[i + 1] if i + 1 < total else None
323
324
  self.append_context_item(
324
325
  meta,
325
326
  item,
326
327
  prev_ctx=prev_ctx,
327
328
  next_ctx=next_item
328
- ) # to html buffer
329
+ )
329
330
  prev_ctx = item
330
- i += 1
331
331
  self.pids[pid].use_buffer = False
332
332
 
333
- # flush
334
333
  if self.pids[pid].html != "":
335
334
  self.append(
336
335
  pid,
337
336
  self.pids[pid].html,
338
337
  flush=True
339
- ) # flush buffer if page loaded, otherwise it will be flushed on page load
338
+ )
340
339
 
341
340
  def append_input(
342
341
  self, meta: CtxMeta,
@@ -352,7 +351,7 @@ class Renderer(BaseRenderer):
352
351
  :param flush: flush HTML
353
352
  :param append: True if force append node
354
353
  """
355
- self.tool_output_end() # reset tools
354
+ self.tool_output_end()
356
355
  pid = self.get_or_create_pid(meta)
357
356
  if not flush:
358
357
  self.clear_chunks_input(pid)
@@ -363,7 +362,6 @@ class Renderer(BaseRenderer):
363
362
 
364
363
  text = ctx.input
365
364
 
366
- # if sub-reply
367
365
  if isinstance(ctx.extra, dict) and "sub_reply" in ctx.extra and ctx.extra["sub_reply"]:
368
366
  try:
369
367
  json_encoded = json.loads(text)
@@ -374,18 +372,16 @@ class Renderer(BaseRenderer):
374
372
  except json.JSONDecodeError:
375
373
  pass
376
374
 
377
- # hidden internal call
378
375
  if ctx.internal \
379
376
  and not ctx.first \
380
377
  and not ctx.input.strip().startswith("user: ") \
381
- and not ctx.input.strip().startswith("@"): # expert says:
378
+ and not ctx.input.strip().startswith("@"):
382
379
  return
383
380
  else:
384
- # don't show user prefix if provided in internal call goal update
385
381
  if ctx.internal and ctx.input.startswith("user: "):
386
382
  text = re.sub(r'^user: ', '> ', ctx.input)
387
383
 
388
- if flush: # to chunk buffer
384
+ if flush:
389
385
  if self.is_stream() and not append:
390
386
  content = self.prepare_node(meta, ctx, text.strip(), self.NODE_INPUT)
391
387
  self.append_chunk_input(meta, ctx, content, False)
@@ -410,17 +406,15 @@ class Renderer(BaseRenderer):
410
406
  :param prev_ctx: previous context
411
407
  :param next_ctx: next context
412
408
  """
413
- self.tool_output_end() # reset tools
409
+ self.tool_output_end()
414
410
  output = ctx.output
415
- if (isinstance(ctx.extra, dict)
416
- and "output" in ctx.extra
417
- and ctx.extra["output"]):
411
+ if isinstance(ctx.extra, dict) and ctx.extra.get("output"):
418
412
  if self.window.core.config.get("llama.idx.chat.agent.render.all", False):
419
- output = "__agent_begin__" + ctx.output + "__agent_end__" + ctx.extra["output"]
413
+ output = "__agent_begin__" + (ctx.output or "") + "__agent_end__" + ctx.extra["output"]
420
414
  else:
421
415
  output = ctx.extra["output"]
422
416
  else:
423
- if ctx.output is None or ctx.output == "":
417
+ if not output:
424
418
  return
425
419
  self.append_node(
426
420
  meta=meta,
@@ -604,38 +598,24 @@ class Renderer(BaseRenderer):
604
598
  self.pids[pid].item = ctx
605
599
  if text_chunk is None or text_chunk == "":
606
600
  if begin:
607
- self.pids[pid].live_buffer = "" # always reset buffer
601
+ self.pids[pid].live_buffer = ""
608
602
  return
609
603
  self.update_names(meta, ctx)
610
- raw_chunk = str(text_chunk)
611
- raw_chunk = raw_chunk.replace("<", "&lt;")
612
- raw_chunk = raw_chunk.replace(">", "&gt;")
604
+ raw_chunk = str(text_chunk).translate({ord('<'): '&lt;', ord('>'): '&gt;'})
613
605
  if begin:
614
- # debug
615
606
  debug = ""
616
607
  if self.is_debug():
617
608
  debug = self.append_debug(ctx, pid, "stream")
618
609
  if debug:
619
610
  raw_chunk = debug + raw_chunk
620
- self.pids[pid].live_buffer = "" # reset buffer
621
- self.pids[pid].is_cmd = False # reset command flag
622
- self.clear_live(meta, ctx) # clear live output
623
- self.pids[pid].live_buffer += raw_chunk
611
+ self.pids[pid].live_buffer = ""
612
+ self.pids[pid].is_cmd = False
613
+ self.clear_live(meta, ctx)
614
+ self.pids[pid].append_live_buffer(raw_chunk)
624
615
 
625
- """
626
- # cooldown (throttling) to prevent high CPU usage on huge text chunks
627
- if len(self.buffer) > self.throttling_min_chars:
628
- current_time = time.time()
629
- if current_time - self.last_time_called <= self.cooldown:
630
- return # wait a moment
631
- else:
632
- self.last_time_called = current_time
633
- """
634
-
635
- # parse chunks
636
616
  to_append = self.pids[pid].live_buffer
637
617
  if has_unclosed_code_tag(self.pids[pid].live_buffer):
638
- to_append += "\n```" # fix for code block without closing ```
618
+ to_append += "\n```"
639
619
  html = self.parser.parse(to_append)
640
620
  escaped_chunk = json.dumps(html)
641
621
  try:
@@ -655,8 +635,7 @@ class Renderer(BaseRenderer):
655
635
  return
656
636
  pid = self.get_or_create_pid(meta)
657
637
  if not self.pids[pid].loaded:
658
- js = "var element = document.getElementById('_append_live_');"
659
- js += "if (element) { element.innerHTML = ''; }"
638
+ js = "var element = document.getElementById('_append_live_');if (element) { element.innerHTML = ''; }"
660
639
  else:
661
640
  js = "clearLive();"
662
641
  try:
@@ -714,11 +693,11 @@ class Renderer(BaseRenderer):
714
693
  """
715
694
  if self.pids[pid].loaded and not self.pids[pid].use_buffer:
716
695
  self.clear_chunks(pid)
717
- self.flush_output(pid, html) # render
696
+ self.flush_output(pid, html)
718
697
  self.pids[pid].html = ""
719
698
  else:
720
699
  if not flush:
721
- self.pids[pid].html += html # to buffer
700
+ self.pids[pid].append_html(html)
722
701
 
723
702
  def append_context_item(
724
703
  self,
@@ -746,7 +725,7 @@ class Renderer(BaseRenderer):
746
725
  flush=False,
747
726
  prev_ctx=prev_ctx,
748
727
  next_ctx=next_ctx
749
- ) # + extra
728
+ )
750
729
 
751
730
  def append_extra(
752
731
  self,
@@ -764,32 +743,28 @@ class Renderer(BaseRenderer):
764
743
  :param render: True if render, False if only return HTML
765
744
  :return: HTML code
766
745
  """
767
- self.tool_output_end() # reset tools
746
+ self.tool_output_end()
768
747
 
769
748
  pid = self.get_pid(meta)
770
- appended = []
771
- html = ""
772
- # images
749
+ appended = set()
750
+ html_parts = []
751
+
773
752
  c = len(ctx.images)
774
753
  if c > 0:
775
754
  n = 1
776
755
  for image in ctx.images:
777
756
  if image is None:
778
757
  continue
779
- # don't append if it is an external url
780
- # if image.startswith("http"):
781
- # continue
782
758
  if image in appended or image in self.pids[pid].images_appended:
783
759
  continue
784
760
  try:
785
- appended.append(image)
786
- html += self.body.get_image_html(image, n, c)
761
+ appended.add(image)
762
+ html_parts.append(self.body.get_image_html(image, n, c))
787
763
  self.pids[pid].images_appended.append(image)
788
764
  n += 1
789
765
  except Exception as e:
790
766
  pass
791
767
 
792
- # files and attachments, TODO check attachments
793
768
  c = len(ctx.files)
794
769
  if c > 0:
795
770
  files_html = []
@@ -798,16 +773,15 @@ class Renderer(BaseRenderer):
798
773
  if file in appended or file in self.pids[pid].files_appended:
799
774
  continue
800
775
  try:
801
- appended.append(file)
776
+ appended.add(file)
802
777
  files_html.append(self.body.get_file_html(file, n, c))
803
778
  self.pids[pid].files_appended.append(file)
804
779
  n += 1
805
780
  except Exception as e:
806
781
  pass
807
782
  if files_html:
808
- html += "<br/>" + "<br/>".join(files_html)
783
+ html_parts.append("<br/>" + "<br/>".join(files_html))
809
784
 
810
- # urls
811
785
  c = len(ctx.urls)
812
786
  if c > 0:
813
787
  urls_html = []
@@ -816,32 +790,33 @@ class Renderer(BaseRenderer):
816
790
  if url in appended or url in self.pids[pid].urls_appended:
817
791
  continue
818
792
  try:
819
- appended.append(url)
793
+ appended.add(url)
820
794
  urls_html.append(self.body.get_url_html(url, n, c))
821
795
  self.pids[pid].urls_appended.append(url)
822
796
  n += 1
823
797
  except Exception as e:
824
798
  pass
825
799
  if urls_html:
826
- html += "<br/>" + "<br/>".join(urls_html)
800
+ html_parts.append("<br/>" + "<br/>".join(urls_html))
827
801
 
828
- # docs json
829
802
  if self.window.core.config.get('ctx.sources'):
830
803
  if ctx.doc_ids is not None and len(ctx.doc_ids) > 0:
831
804
  try:
832
805
  docs = self.body.get_docs_html(ctx.doc_ids)
833
- html += docs
806
+ html_parts.append(docs)
834
807
  except Exception as e:
835
808
  pass
836
- # flush
809
+
810
+ html = "".join(html_parts)
837
811
  if render and html != "":
838
812
  if footer:
839
- # append to output
840
813
  self.append(pid, html)
841
814
  else:
842
- # append to existing message box using JS
843
815
  escaped_html = json.dumps(html)
844
- self.get_output_node(meta).page().runJavaScript("appendExtra('{}',{});".format(ctx.id, escaped_html))
816
+ try:
817
+ self.get_output_node(meta).page().runJavaScript("appendExtra('{}',{});".format(ctx.id, escaped_html))
818
+ except Exception as e:
819
+ pass
845
820
 
846
821
  return html
847
822
 
@@ -881,16 +856,13 @@ class Renderer(BaseRenderer):
881
856
  :param meta: Context meta
882
857
  """
883
858
  pid = self.get_pid(meta)
884
- if pid is not None and pid in self.pids: # in PIDs only if at least one ctx item is appended
859
+ if pid is not None and pid in self.pids:
885
860
  self.reset_by_pid(pid)
886
861
  else:
887
- # there is no pid here if empty context so check for meta, and clear current
888
862
  if meta is not None:
889
- # create new PID using only meta
890
863
  pid = self.get_or_create_pid(meta)
891
864
  self.reset_by_pid(pid)
892
865
 
893
- # clear live output
894
866
  self.clear_live(meta, CtxItem())
895
867
 
896
868
  def reset_by_pid(self, pid: Optional[int]):
@@ -907,7 +879,9 @@ class Renderer(BaseRenderer):
907
879
  self.pids[pid].images_appended = []
908
880
  self.pids[pid].urls_appended = []
909
881
  self.pids[pid].files_appended = []
910
- self.get_output_node_by_pid(pid).reset_current_content()
882
+ node = self.get_output_node_by_pid(pid)
883
+ if node is not None:
884
+ node.reset_current_content()
911
885
  self.reset_names_by_pid(pid)
912
886
  self.prev_chunk_replace = False
913
887
 
@@ -950,8 +924,7 @@ class Renderer(BaseRenderer):
950
924
  if pid is None:
951
925
  return
952
926
  if not self.pids[pid].loaded:
953
- js = "var element = document.getElementById('_append_input_');"
954
- js += "if (element) { element.innerHTML = ''; }"
927
+ js = "var element = document.getElementById('_append_input_');if (element) { element.innerHTML = ''; }"
955
928
  else:
956
929
  js = "clearInput();"
957
930
  try:
@@ -970,8 +943,7 @@ class Renderer(BaseRenderer):
970
943
  """
971
944
  self.prev_chunk_replace = False
972
945
  if not self.pids[pid].loaded:
973
- js = "var element = document.getElementById('_append_output_');"
974
- js += "if (element) { element.innerHTML = ''; }"
946
+ js = "var element = document.getElementById('_append_output_');if (element) { element.innerHTML = ''; }"
975
947
  else:
976
948
  js = "clearOutput();"
977
949
  try:
@@ -989,8 +961,7 @@ class Renderer(BaseRenderer):
989
961
  :pid: context PID
990
962
  """
991
963
  if not self.pids[pid].loaded:
992
- js = "var element = document.getElementById('_nodes_');"
993
- js += "if (element) { element.innerHTML = ''; }"
964
+ js = "var element = document.getElementById('_nodes_');if (element) { element.innerHTML = ''; }"
994
965
  else:
995
966
  js = "clearNodes();"
996
967
  try:
@@ -1069,7 +1040,6 @@ class Renderer(BaseRenderer):
1069
1040
  if type(ctx.extra) is dict and "agent_evaluate" in ctx.extra:
1070
1041
  name = trans("msg.name.evaluation")
1071
1042
 
1072
- # debug
1073
1043
  debug = ""
1074
1044
  if self.is_debug():
1075
1045
  debug = self.append_debug(ctx, pid, "input")
@@ -1080,21 +1050,14 @@ class Renderer(BaseRenderer):
1080
1050
  extra = ctx.extra["footer"]
1081
1051
  extra_style = "display:block;"
1082
1052
  html = (
1083
- '<div class="msg-box msg-user" id="{msg_id}">'
1084
- '<div class="name-header name-user">{name}</div>'
1085
- '<div class="msg">'
1086
- '{html}'
1087
- '<div class="msg-extra" style="{extra_style}">{extra}</div>'
1088
- '{debug}'
1089
- '</div>'
1090
- '</div>'
1091
- ).format(
1092
- msg_id=msg_id,
1093
- name=name,
1094
- html=html,
1095
- extra=extra,
1096
- extra_style=extra_style,
1097
- debug=debug,
1053
+ f'<div class="msg-box msg-user" id="{msg_id}">'
1054
+ f'<div class="name-header name-user">{name}</div>'
1055
+ f'<div class="msg">'
1056
+ f'{html}'
1057
+ f'<div class="msg-extra" style="{extra_style}">{extra}</div>'
1058
+ f'{debug}'
1059
+ f'</div>'
1060
+ f'</div>'
1098
1061
  )
1099
1062
 
1100
1063
  return html
@@ -1117,17 +1080,13 @@ class Renderer(BaseRenderer):
1117
1080
  :param next_ctx: next context item
1118
1081
  :return: prepared HTML
1119
1082
  """
1120
- is_cmd = False
1121
- if (
1122
- next_ctx is not None and
1123
- next_ctx.internal and
1124
- (len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
1125
- ):
1126
- is_cmd = True
1083
+ is_cmd = (
1084
+ next_ctx is not None and
1085
+ next_ctx.internal and
1086
+ (len(ctx.cmds) > 0 or (ctx.extra_ctx is not None and len(ctx.extra_ctx) > 0))
1087
+ )
1127
1088
  pid = self.get_or_create_pid(meta)
1128
1089
  msg_id = "msg-bot-" + str(ctx.id) if ctx is not None else ""
1129
- # if is_cmd:
1130
- # html = self.helpers.format_cmd_text(html)
1131
1090
  html = self.helpers.pre_format_text(html)
1132
1091
  html = self.parser.parse(html)
1133
1092
  html = self.append_timestamp(ctx, html, type=self.NODE_OUTPUT)
@@ -1135,42 +1094,28 @@ class Renderer(BaseRenderer):
1135
1094
  extra = self.append_extra(meta, ctx, footer=True, render=False)
1136
1095
  footer = self.body.prepare_action_icons(ctx)
1137
1096
 
1138
- # append tool output
1139
1097
  tool_output = ""
1140
1098
  spinner = ""
1141
- icon = os.path.join(
1142
- self.window.core.config.get_app_path(),
1143
- "data", "icons", "expand.svg"
1144
- )
1145
1099
  output_class = "display:none"
1146
- cmd_icon = (
1147
- '<img src="file://{}" width="25" height="25" valign="middle">'
1148
- .format(icon)
1149
- )
1100
+ cmd_icon = f'<img src="{self._file_prefix}{self._icon_expand}" width="25" height="25" valign="middle">'
1150
1101
  expand_btn = (
1151
- "<span class='toggle-cmd-output' onclick='toggleToolOutput({});' title='{}' "
1152
- "role='button'>{}</span>"
1153
- .format(str(ctx.id), trans('action.cmd.expand'), cmd_icon)
1102
+ f"<span class='toggle-cmd-output' onclick='toggleToolOutput({str(ctx.id)});' title='{trans('action.cmd.expand')}' "
1103
+ f"role='button'>{cmd_icon}</span>"
1154
1104
  )
1155
1105
 
1156
- # check if next ctx is internal and current ctx has commands
1157
1106
  if is_cmd:
1158
- # first, check current input if agent step and results
1159
1107
  if ctx.results is not None and len(ctx.results) > 0 \
1160
1108
  and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
1161
1109
  tool_output = self.helpers.format_cmd_text(str(ctx.input))
1162
- output_class = "" # show tool output
1110
+ output_class = ""
1163
1111
  else:
1164
- # get output from next input (JSON response)
1165
1112
  tool_output = self.helpers.format_cmd_text(str(next_ctx.input))
1166
- output_class = "" # show tool output
1113
+ output_class = ""
1167
1114
 
1168
- # check if agent step and results in current ctx
1169
1115
  elif ctx.results is not None and len(ctx.results) > 0 \
1170
1116
  and isinstance(ctx.extra, dict) and "agent_step" in ctx.extra:
1171
1117
  tool_output = self.helpers.format_cmd_text(str(ctx.input))
1172
1118
  else:
1173
- # loading spinner
1174
1119
  if (
1175
1120
  next_ctx is None and
1176
1121
  (
@@ -1181,22 +1126,15 @@ class Renderer(BaseRenderer):
1181
1126
  len(ctx.cmds) > 0
1182
1127
  )
1183
1128
  ):
1184
- spinner_class = "display:none" # hide by default
1185
- if ctx.live:
1186
- spinner_class = "" # show spinner only if commands and active run
1187
- icon = os.path.join(
1188
- self.window.core.config.get_app_path(),
1189
- "data", "icons", "sync.svg"
1190
- )
1129
+ spinner_class = "" if ctx.live else "display:none"
1191
1130
  spinner = (
1192
- '<span class="spinner" style="{}">'
1193
- '<img src="file://{}" width="30" height="30" '
1194
- 'class="loading"></span>'
1195
- .format(spinner_class, icon)
1131
+ f'<span class="spinner" style="{spinner_class}">'
1132
+ f'<img src="{self._file_prefix}{self._icon_sync}" width="30" height="30" '
1133
+ f'class="loading"></span>'
1196
1134
  )
1197
1135
 
1198
1136
  html_tools = (
1199
- '<div class="tool-output" style="{}">'.format(output_class) +
1137
+ f'<div class="tool-output" style="{output_class}">' +
1200
1138
  expand_btn +
1201
1139
  '<div class="content" style="display:none">' +
1202
1140
  tool_output +
@@ -1204,33 +1142,24 @@ class Renderer(BaseRenderer):
1204
1142
  )
1205
1143
  tool_extra = self.body.prepare_tool_extra(ctx)
1206
1144
 
1207
- # debug
1208
1145
  debug = ""
1209
1146
  if self.is_debug():
1210
1147
  debug = self.append_debug(ctx, pid, "output")
1211
1148
 
1212
1149
  name_header = self.get_name_header(ctx)
1213
1150
  html = (
1214
- '<div class="msg-box msg-bot" id="{msg_id}">'
1215
- + name_header +
1216
- '<div class="msg">'
1217
- '{html}'
1218
- '<div class="msg-tool-extra">{tool_extra}</div>'
1219
- '{html_tools}'
1220
- '<div class="msg-extra">{extra}</div>'
1221
- '{footer}'
1222
- '{debug}'
1223
- '</div>'
1151
+ f'<div class="msg-box msg-bot" id="{msg_id}">' +
1152
+ name_header +
1153
+ '<div class="msg">' +
1154
+ f'{html}' +
1155
+ f'{spinner}' +
1156
+ f'<div class="msg-tool-extra">{tool_extra}</div>' +
1157
+ f'{html_tools}' +
1158
+ f'<div class="msg-extra">{extra}</div>' +
1159
+ f'{footer}' +
1160
+ f'{debug}' +
1161
+ '</div>' +
1224
1162
  '</div>'
1225
- ).format(
1226
- msg_id=msg_id,
1227
- name_bot=self.pids[pid].name_bot,
1228
- html=html,
1229
- html_tools=html_tools,
1230
- extra=extra,
1231
- footer=footer,
1232
- tool_extra=tool_extra,
1233
- debug=debug,
1234
1163
  )
1235
1164
 
1236
1165
  return html
@@ -1263,11 +1192,7 @@ class Renderer(BaseRenderer):
1263
1192
  avatars_dir = os.path.join(presets_dir, "avatars")
1264
1193
  avatar_path = os.path.join(avatars_dir, preset.ai_avatar)
1265
1194
  if os.path.exists(avatar_path):
1266
- if self.window.core.platforms.is_windows():
1267
- prefix = 'file:///'
1268
- else:
1269
- prefix = 'file://'
1270
- avatar_html = "<img src=\"" + prefix + avatar_path + "\" class=\"avatar\"> "
1195
+ avatar_html = f"<img src=\"{self._file_prefix}{avatar_path}\" class=\"avatar\"> "
1271
1196
 
1272
1197
  if not output_name and not avatar_html:
1273
1198
  return ""
@@ -1288,13 +1213,12 @@ class Renderer(BaseRenderer):
1288
1213
  try:
1289
1214
  node = self.get_output_node_by_pid(pid)
1290
1215
  node.page().runJavaScript(f"if (typeof window.appendNode !== 'undefined') appendNode({escaped_html});")
1291
- node.update_current_content()
1292
1216
  except Exception as e:
1293
1217
  pass
1294
1218
 
1295
1219
  def reload(self):
1296
1220
  """Reload output, called externally only on theme change to redraw content"""
1297
- self.window.controller.ctx.refresh_output() # if clear all and appends all items again
1221
+ self.window.controller.ctx.refresh_output()
1298
1222
 
1299
1223
  def flush(
1300
1224
  self,
@@ -1307,11 +1231,13 @@ class Renderer(BaseRenderer):
1307
1231
  :param pid: context PID
1308
1232
  """
1309
1233
  if self.pids[pid].loaded:
1310
- return # wait for page load
1234
+ return
1311
1235
 
1312
1236
  html = self.body.get_html(pid)
1313
1237
  self.pids[pid].document = html
1314
- self.get_output_node_by_pid(pid).setHtml(html, baseUrl="file://")
1238
+ node = self.get_output_node_by_pid(pid)
1239
+ if node is not None:
1240
+ node.setHtml(html, baseUrl="file://")
1315
1241
 
1316
1242
  def fresh(
1317
1243
  self,
@@ -1329,8 +1255,9 @@ class Renderer(BaseRenderer):
1329
1255
  self.pids[pid].loaded = False
1330
1256
  self.pids[pid].document = html
1331
1257
  node = self.get_output_node_by_pid(pid)
1332
- node.resetPage()
1333
- node.setHtml(html, baseUrl="file://")
1258
+ if node is not None:
1259
+ node.resetPage()
1260
+ node.setHtml(html, baseUrl="file://")
1334
1261
 
1335
1262
  def get_output_node(
1336
1263
  self,
@@ -1378,7 +1305,9 @@ class Renderer(BaseRenderer):
1378
1305
  if pid is None:
1379
1306
  return ""
1380
1307
  if plain:
1381
- return self.parser.to_plain_text(self.pids[pid].document.replace("<br>", "\n"))
1308
+ return self.parser.to_plain_text(
1309
+ self.pids[pid].document.replace("<br>", "\n").replace("<br/>", "\n")
1310
+ )
1382
1311
  return self.pids[pid].document
1383
1312
 
1384
1313
  def remove_item(self, ctx: CtxItem):
@@ -1388,8 +1317,9 @@ class Renderer(BaseRenderer):
1388
1317
  :param ctx: context item
1389
1318
  """
1390
1319
  try:
1320
+ _id = json.dumps(ctx.id)
1391
1321
  self.get_output_node(ctx.meta).page().runJavaScript(
1392
- "if (typeof window.removeNode !== 'undefined') removeNode({});".format(ctx.id))
1322
+ f"if (typeof window.removeNode !== 'undefined') removeNode({_id});")
1393
1323
  except Exception as e:
1394
1324
  pass
1395
1325
 
@@ -1400,8 +1330,9 @@ class Renderer(BaseRenderer):
1400
1330
  :param ctx: context item
1401
1331
  """
1402
1332
  try:
1333
+ _id = json.dumps(ctx.id)
1403
1334
  self.get_output_node(ctx.meta).page().runJavaScript(
1404
- "if (typeof window.removeNodesFromId !== 'undefined') removeNodesFromId({});".format(ctx.id))
1335
+ f"if (typeof window.removeNodesFromId !== 'undefined') removeNodesFromId({_id});")
1405
1336
  except Exception as e:
1406
1337
  pass
1407
1338
 
@@ -1431,7 +1362,6 @@ class Renderer(BaseRenderer):
1431
1362
 
1432
1363
  :param ctx: context item
1433
1364
  """
1434
- # remove all items from ID
1435
1365
  self.remove_items_from(ctx)
1436
1366
 
1437
1367
  def on_edit_submit(self, ctx: CtxItem):
@@ -1440,7 +1370,6 @@ class Renderer(BaseRenderer):
1440
1370
 
1441
1371
  :param ctx: context item
1442
1372
  """
1443
- # remove all items from ID
1444
1373
  self.remove_items_from(ctx)
1445
1374
 
1446
1375
  def on_enable_edit(self, live: bool = True):
@@ -1564,12 +1493,12 @@ class Renderer(BaseRenderer):
1564
1493
  for node in nodes:
1565
1494
  try:
1566
1495
  node.page().runJavaScript(
1567
- "if (typeof window.updateCSS !== 'undefined') updateCSS({});".format(to_json))
1496
+ f"if (typeof window.updateCSS !== 'undefined') updateCSS({to_json});")
1568
1497
  if self.window.core.config.get('render.blocks'):
1569
1498
  node.page().runJavaScript("if (typeof window.enableBlocks !== 'undefined') enableBlocks();")
1570
1499
  else:
1571
1500
  node.page().runJavaScript(
1572
- "if (typeof window.disableBlocks !== 'undefined') disableBlocks();") # TODO: ctx!!!!!
1501
+ "if (typeof window.disableBlocks !== 'undefined') disableBlocks();")
1573
1502
  except Exception as e:
1574
1503
  pass
1575
1504
  return
@@ -1681,4 +1610,4 @@ class Renderer(BaseRenderer):
1681
1610
  Remove PID from renderer
1682
1611
  """
1683
1612
  if pid in self.pids:
1684
- del self.pids[pid]
1613
+ del self.pids[pid]