Skip to content

Commit 057e38c

Browse files
authored
Merge pull request #182 from FrancescoCaracciolo/master
LaTeX inline rendering support, Gemini improvements
2 parents e1db9d0 + 4eb6e33 commit 057e38c

File tree

9 files changed

+209
-54
lines changed

9 files changed

+209
-54
lines changed

src/constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,9 +296,10 @@
296296
| - | 1 | 2 | 3 | 4 |\n| - | - | - | - | - |\n| 1 | 1 | 2 | 3 | 4 |\n| 2 | 2 | 4 | 6 | 8 |\n| 3 | 3 | 6 | 9 | 12 |\n| 4 | 4 | 8 | 12 | 16 |
297297
298298
You can write codeblocks:
299-
```cpp\n#include<iostream>\nusing namespace std;\nint main(){\n cout<<"Hello world!";\n return 0;\n}\n```
299+
```python\nprint("hello")\n```
300300
301-
You can also use **bold**, *italic*, ~strikethrough~, `monospace`, [linkname](https://link.com) and ## headers in markdown
301+
You can also use **bold**, *italic*, ~strikethrough~, `monospace`, [linkname](https://link.com) and ## headers in markdown.
302+
You can display $inline equations$ and $$equations$$.
302303
""",
303304
"show_image": """You can show the user an image, if needed, using \n```image\npath\n```\n\nYou can show the user a video, if needed, using\n```video\npath\n```""",
304305
"graphic": """System: You can display the graph using this structure: ```chart\n name - value\n ... \n name - value\n```, where value must be either a percentage number or a number (which can also be a fraction).

src/handlers/llm/gemini_handler.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def get_models(self, manual=False):
5959
models = client.models.list()
6060
result = tuple()
6161
for model in models:
62+
print(model.supported_actions)
63+
print(model)
6264
if "embedding" in model.display_name.lower() or "legacy" in model.display_name.lower():
6365
continue
6466
result += ((model.display_name, model.name,),)
@@ -128,10 +130,9 @@ def get_extra_settings(self) -> list:
128130
]
129131
if self.get_setting("advanced_params", False):
130132
r += [
131-
ExtraSettings.ScaleSetting("temperature", "Temperature", "Creativity allowed in the responses", 1, 0, 2, 1),
132-
ExtraSettings.ScaleSetting("top_p", "Top P", "Probability of the top tokens to keep", 1, 0, 1, 1),
133-
ExtraSettings.ScaleSetting("max_tokens", "Max Tokens", "Maximum number of tokens to generate", 8192, 0, 8192, 1),
134-
ExtraSettings.ScaleSetting("frequency-penalty", "Frequency Penalty", "Frequency penalty", 1, 0, 2, 1),
133+
ExtraSettings.ScaleSetting("temperature", "Temperature", "Creativity allowed in the responses", 1, 0, 2, 2),
134+
ExtraSettings.ScaleSetting("top_p", "Top P", "Probability of the top tokens to keep", 1, 0, 1, 2),
135+
ExtraSettings.ScaleSetting("max_tokens", "Max Tokens", "Maximum number of tokens to generate", 8192, 0, 65536, 0),
135136
]
136137
return r
137138
def __convert_history(self, history: list):
@@ -220,23 +221,25 @@ def generate_text(self, prompt: str, history: list[dict[str, str]] = [], system_
220221

221222
def generate_text_stream(self, prompt: str, history: list[dict[str, str]] = [], system_prompt: list[str] = [], on_update: Callable[[str], Any] = lambda _: None , extra_args: list = []) -> str:
222223
from google import genai
223-
from google.genai.types import HarmCategory, HarmBlockThreshold, GenerateContentConfig, Part
224+
from google.genai.types import HarmCategory, HarmBlockThreshold, GenerateContentConfig, Part
225+
from google.genai import types
224226
if self.get_setting("safety"):
225227
safety = None
226228
else:
227-
safety = {
228-
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
229-
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
230-
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
231-
}
232-
229+
safety = [
230+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=HarmBlockThreshold.BLOCK_NONE),
231+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold=HarmBlockThreshold.BLOCK_NONE),
232+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY, threshold=HarmBlockThreshold.BLOCK_NONE),
233+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=HarmBlockThreshold.BLOCK_NONE),
234+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold=HarmBlockThreshold.BLOCK_NONE),
235+
]
233236
client = genai.Client(api_key=self.get_setting("apikey"))
234237
instructions = "\n".join(system_prompt)
235238
append_instructions = None
236239
if not self.get_setting("system_prompt"):
237240
instructions = None
238241
if self.get_setting("force_system_prompt"):
239-
append_instructions = "\n".join(system_prompt)
242+
append_instructions = "\n".join([p.replace("```", "\\\\\\```") for p in system_prompt])
240243
if not self.get_setting("advanced_params"):
241244
generate_content_config = GenerateContentConfig( system_instruction=instructions,
242245
safety_settings=safety, response_modalities=["text"] + ["image"] if self.get_setting("img_output") else ["text"])

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ widgets_sources = [
5656
'ui/widgets/file.py',
5757
'ui/widgets/latex.py',
5858
'ui/widgets/terminal_dialog.py',
59+
'ui/widgets/markuptextview.py',
5960
]
6061

6162
handler_sources = [

src/ui/widgets/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .comborow import ComboRowHelper
55
from .copybox import CopyBox
66
from .file import File
7-
from .latex import DisplayLatex
7+
from .latex import DisplayLatex, LatexCanvas, InlineLatex
8+
from .markuptextview import MarkupTextView
89

9-
__all__ = ["ProfileRow", "MultilineEntry", "BarChartBox", "ComboRowHelper", "CopyBox", "File", "DisplayLatex"]
10+
__all__ = ["ProfileRow", "MultilineEntry", "BarChartBox", "ComboRowHelper", "CopyBox", "File", "DisplayLatex", "LatexCanvas", "MarkupTextView", "InlineLatex"]

src/ui/widgets/latex.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,70 @@
99

1010

1111
class LatexCanvas(FigureCanvasGTK4Agg):
12-
def __init__(self, latex: str, size: int, color):
12+
def __init__(self, latex: str, size: int, color, inline: bool = False) -> None:
1313
fig = Figure()
1414
fig.patch.set_alpha(0)
1515
ax = fig.add_subplot()
1616
txt = ax.text(0.5, 0.5, r'$' + latex + r'$', fontsize=size, ha='center', va='center', color=(color.red, color.blue, color.green))
1717
ax.axis('off')
18-
fig.tight_layout(pad=0)
18+
fig.tight_layout()
1919
fig.canvas.draw()
2020
fig_size = txt.get_window_extent()
2121
h = int(fig_size.height)
2222
w = int(fig_size.width)
23+
self.dims = (w, h)
2324
super().__init__(fig)
2425
self.set_hexpand(True)
2526
self.set_vexpand(True)
26-
self.set_size_request(w, h + int(h * (0.1)))
27+
if inline:
28+
self.set_halign(Gtk.Align.START)
29+
self.set_valign(Gtk.Align.END)
30+
self.set_size_request(w, h)
31+
else:
32+
self.set_size_request(w, h + int(h * (0.1)))
2733
self.set_css_classes(['latex_renderer'])
2834

35+
class InlineLatex(Gtk.Box):
36+
37+
def __init__(self, latex: str, size: int) -> None:
38+
super().__init__()
39+
self.color = self.get_style_context().lookup_color("window_fg_color")[1]
40+
self.latex = latex
41+
self.size = size
42+
self.picture = LatexCanvas(latex, self.size, self.color, inline=True)
43+
if self.picture.dims[0] > 300:
44+
scroll = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER, propagate_natural_height=True, hscrollbar_policy=Gtk.PolicyType.AUTOMATIC, propagate_natural_width=True, hexpand=True)
45+
scroll.set_child(self.picture)
46+
scroll.set_size_request(300, -1)
47+
self.append(scroll)
48+
else:
49+
self.append(self.picture)
50+
51+
2952
class DisplayLatex(Gtk.Box):
3053

31-
def __init__(self, latex:str, size:int, cache_dir: str) -> None:
54+
def __init__(self, latex:str, size:int, cache_dir: str, inline: bool = False) -> None:
3255
super().__init__()
3356
self.cachedir = cache_dir
3457
self.size = size
3558

36-
overlay = Gtk.Overlay()
3759

3860
self.latex = latex
3961
self.color = self.get_style_context().lookup_color("window_fg_color")[1]
4062
# Create Gtk.Picture
41-
self.scroll = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER, propagate_natural_height=True, hscrollbar_policy=Gtk.PolicyType.AUTOMATIC, propagate_natural_width=True)
42-
self.picture = LatexCanvas(latex, self.size, self.color)
43-
self.scroll.set_child(self.picture)
44-
self.create_control_box()
45-
self.controller()
46-
overlay.set_child(self.scroll)
47-
overlay.add_overlay(self.control_box)
48-
self.overlay = overlay
49-
self.append(overlay)
63+
self.picture = LatexCanvas(latex, self.size, self.color, inline)
64+
if not inline:
65+
self.scroll = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER, propagate_natural_height=True, hscrollbar_policy=Gtk.PolicyType.AUTOMATIC, propagate_natural_width=True)
66+
self.scroll.set_child(self.picture)
67+
self.create_control_box()
68+
self.controller()
69+
overlay = Gtk.Overlay()
70+
overlay.set_child(self.scroll)
71+
overlay.add_overlay(self.control_box)
72+
self.overlay = overlay
73+
self.append(overlay)
74+
else:
75+
self.append(self.picture)
5076

5177

5278
def zoom_in(self, *_):

src/ui/widgets/markuptextview.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from gi.repository import Gtk, Pango, Gdk
2+
import xml.etree.ElementTree as ET
3+
from .. import apply_css_to_widget
4+
5+
class MarkupTextView(Gtk.TextView):
6+
def __init__(self, parent):
7+
super().__init__()
8+
self.set_wrap_mode(Gtk.WrapMode.WORD)
9+
self.set_editable(False)
10+
self.set_cursor_visible(False)
11+
12+
self.buffer = self.get_buffer()
13+
self.create_tags()
14+
#self.buffer.connect("changed", lambda b: self.update_textview_size(parent))
15+
self.add_css_class("scroll")
16+
apply_css_to_widget(
17+
self, ".scroll { background-color: rgba(0,0,0,0);}"
18+
)
19+
20+
def update_textview_size(self, parent=None):
21+
if parent is not None:
22+
s = parent.get_size(Gtk.Orientation.HORIZONTAL)
23+
else:
24+
s = 300
25+
buffer = self.get_buffer()
26+
layout = self.create_pango_layout(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True))
27+
layout.set_width(s * Pango.SCALE)
28+
layout.set_wrap(Pango.WrapMode.WORD)
29+
width, height = layout.get_pixel_size()
30+
self.set_size_request(width, height)
31+
32+
def create_tags(self):
33+
self.tags = {
34+
'b': self.buffer.create_tag("bold", weight=Pango.Weight.BOLD),
35+
'i': self.buffer.create_tag("italic", style=Pango.Style.ITALIC),
36+
'tt': self.buffer.create_tag("tt", family="monospace"),
37+
'sub': self.buffer.create_tag("sub", rise=-5000, size_points=8),
38+
'sup': self.buffer.create_tag("sup", rise=5000, size_points=8),
39+
'a': self.buffer.create_tag("link", foreground="blue", underline=Pango.Underline.SINGLE)
40+
}
41+
42+
def add_markup_text(self, iter, text: str):
43+
wrapped_markup = f"<root>{text}</root>"
44+
try:
45+
root = ET.fromstring(wrapped_markup)
46+
except ET.ParseError as e:
47+
print("Parse error:", e)
48+
self.buffer.insert(iter, text)
49+
return
50+
self._insert_markup_recursive(root, iter, [])
51+
52+
def set_markup(self, markup: str):
53+
# Wrap in a root tag for parsing
54+
wrapped_markup = f"<root>{markup}</root>"
55+
try:
56+
root = ET.fromstring(wrapped_markup)
57+
except ET.ParseError as e:
58+
print("Parse error:", e)
59+
return
60+
61+
self.buffer.set_text("")
62+
self._insert_markup_recursive(root, self.buffer.get_start_iter(), [])
63+
64+
def _insert_markup_recursive(self, elem, iter, active_tags):
65+
# Add text before children
66+
if elem.text:
67+
self.buffer.insert_with_tags(iter, elem.text, *active_tags)
68+
69+
# Process children recursively
70+
for child in elem:
71+
tag_name = child.tag.lower()
72+
tags_to_apply = list(active_tags)
73+
74+
if tag_name == "a":
75+
tags_to_apply.append(self.tags["a"])
76+
elif tag_name in self.tags:
77+
tags_to_apply.append(self.tags[tag_name])
78+
79+
self._insert_markup_recursive(child, iter, tags_to_apply)
80+
81+
# Tail text after this child
82+
if child.tail:
83+
self.buffer.insert_with_tags(iter, child.tail, *active_tags)

src/utility/message_chunk.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,11 @@ def process_inline_elements(text: str, allow_latex: bool) -> List[MessageChunk]:
217217
# Add the inline latex chunk
218218
content = m.group(1) or m.group(2) # Group 1 for $..$, Group 2 for \(..\)
219219
if content is not None:
220-
chunks.append(MessageChunk(type="latex_inline", text=content.strip()))
220+
equation = content.strip()
221+
if len(content) < 40:
222+
chunks.append(MessageChunk(type="latex_inline", text=content.strip()))
223+
else:
224+
chunks.append(MessageChunk(type="latex", text=content.strip()))
221225
last_index = end
222226

223227
# Add any remaining text after the last match
@@ -337,11 +341,22 @@ def get_message_chunks(message: str, allow_latex: bool = True) -> List[MessageCh
337341
# Filter truly empty text chunks that might remain after merging
338342
merged_flat_chunks = [c for c in merged_flat_chunks if c.type != "text" or c.text != ""]
339343

340-
341344
# Now group the merged flat list
342345
for chunk in merged_flat_chunks:
346+
append_next = None
343347
is_inline_constituent = chunk.type in ("text", "latex_inline")
344348

349+
# Check if the next block is a latex_inline
350+
if chunk.type == "text":
351+
current_index = merged_flat_chunks.index(chunk)
352+
if current_index < len(merged_flat_chunks) - 2:
353+
next_chunk = merged_flat_chunks[current_index + 1]
354+
if next_chunk.type == "latex_inline":
355+
lines = chunk.text.split("\n")
356+
if len([line for line in lines if line != "" ]) > 1:
357+
chunk.text = "\n".join(lines[:-1])
358+
append_next = MessageChunk(type="text", text=lines[-1])
359+
is_inline_constituent = False
345360
if is_inline_constituent:
346361
current_inline_sequence.append(chunk)
347362
else:
@@ -357,7 +372,9 @@ def get_message_chunks(message: str, allow_latex: bool = True) -> List[MessageCh
357372
# Only a single text chunk, add it directly
358373
grouped_chunks.append(current_inline_sequence[0])
359374
current_inline_sequence = [] # Reset sequence
360-
375+
if append_next is not None:
376+
current_inline_sequence.append(append_next)
377+
361378
# Add the non-inline chunk
362379
grouped_chunks.append(chunk)
363380

src/utility/strings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ def markwon_to_pango(markdown_text):
3030
# Escape potential Pango/XML characters first to avoid issues
3131
# with user input containing <, >, &
3232
escaped_text = GLib.markup_escape_text(markdown_text)
33+
escaped_text = escaped_text.replace("&lt;sub&gt;", "<sub>" )
34+
escaped_text = escaped_text.replace("&lt;/sub&gt;", "</sub>" )
35+
36+
escaped_text = escaped_text.replace("&lt;sup&gt;", "<sup>" )
37+
escaped_text = escaped_text.replace("&lt;/sup&gt;", "</sup>" )
3338
initial_string = escaped_text # Keep the escaped version as fallback
3439

3540
processed_text = escaped_text

0 commit comments

Comments
 (0)