Add builder UI
This commit is contained in:
@@ -1 +1,2 @@
|
||||
__pycache__/
|
||||
*~
|
||||
@@ -1,43 +0,0 @@
|
||||
import gi
|
||||
from gi.repository import GLib, Gtk, Gdk
|
||||
|
||||
import util
|
||||
|
||||
class ChatView(Gtk.ScrolledWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cur_ai_response = None
|
||||
|
||||
self.listbox = Gtk.ListBox()
|
||||
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
self.listbox.set_focusable(True)
|
||||
|
||||
self.set_child(self.listbox)
|
||||
|
||||
def scroll_to_bottom(self, *args):
|
||||
adj = self.get_vadjustment()
|
||||
adj.set_value(adj.get_upper() - adj.get_page_size())
|
||||
|
||||
def start_response(self):
|
||||
self.cur_ai_response = self.add_message(sender=False)
|
||||
return self.cur_ai_response
|
||||
|
||||
def add_message(self, text="", sender=True):
|
||||
label = Gtk.Label(selectable=True, wrap=True, xalign=1.0 if sender else 0.0)
|
||||
label.set_text(text)
|
||||
util.set_margin_all(label, 8)
|
||||
|
||||
row = Gtk.ListBoxRow()
|
||||
row.set_child(label)
|
||||
self.listbox.append(row)
|
||||
|
||||
GLib.idle_add(self.scroll_to_bottom)
|
||||
return label
|
||||
|
||||
def add_response_part(self, part):
|
||||
if self.cur_ai_response is None:
|
||||
return
|
||||
|
||||
self.cur_ai_response.set_label(self.cur_ai_response.get_label() + part)
|
||||
GLib.idle_add(self.scroll_to_bottom)
|
||||
@@ -2,86 +2,114 @@ import gi, ollama, asyncio, threading
|
||||
gi.require_version("Gtk", "4.0")
|
||||
from gi.repository import GLib, Gtk, Gdk
|
||||
|
||||
from prompt import PromptView
|
||||
from chat import ChatView
|
||||
from preferences import ModelPreferenceView
|
||||
import util
|
||||
|
||||
@Gtk.Template(filename="pyllama.ui")
|
||||
class PyLlamaWindow(Gtk.ApplicationWindow):
|
||||
__gtype_name__ = "main_window"
|
||||
|
||||
model_list = Gtk.Template.Child("model_list")
|
||||
model_parameters = Gtk.Template.Child("model_parameters")
|
||||
chat_scroll = Gtk.Template.Child("chat_scroll")
|
||||
chat_list = Gtk.Template.Child("chat_list")
|
||||
prompt = Gtk.Template.Child("prompt")
|
||||
prompt_controller = Gtk.Template.Child("prompt_controller")
|
||||
send_button = Gtk.Template.Child("send_button")
|
||||
cancel_button = Gtk.Template.Child("cancel_button")
|
||||
gpu_layers_spin = Gtk.Template.Child("gpu_layers_spin")
|
||||
low_vram_switch = Gtk.Template.Child("low_vram_switch")
|
||||
|
||||
def __init__(self, application=None, title=None):
|
||||
super().__init__(application=application, title=title)
|
||||
self.model_list.set_model(self.get_application().model_store)
|
||||
self.prompt.add_controller(self.prompt_controller)
|
||||
|
||||
self.set_default_size(800, 600)
|
||||
self._current_response = None
|
||||
|
||||
self.model_store = Gtk.StringList()
|
||||
self.model_dropdown = Gtk.DropDown(tooltip_text="Select model")
|
||||
self.model_dropdown.set_model(self.model_store)
|
||||
@property
|
||||
def prompt_buffer(self):
|
||||
return self.prompt.get_buffer()
|
||||
|
||||
model_preferences = Gtk.Button(icon_name="view-more-symbolic",
|
||||
tooltip_text="Model preferences")
|
||||
model_preferences.connect("clicked", self.on_preferences_clicked)
|
||||
def prompt_clear(self):
|
||||
self.prompt_buffer.set_text("")
|
||||
|
||||
self.model_prefs = ModelPreferenceView(position=Gtk.PositionType.BOTTOM,
|
||||
on_gpu_change=self.on_gpu_changed)
|
||||
self.model_prefs.set_parent(model_preferences)
|
||||
def prompt_get_text(self):
|
||||
start = self.prompt_buffer.get_start_iter()
|
||||
end = self.prompt_buffer.get_end_iter()
|
||||
return self.prompt_buffer.get_text(start, end, False).strip()
|
||||
|
||||
header = Gtk.HeaderBar(show_title_buttons=True)
|
||||
header.pack_start(self.model_dropdown)
|
||||
header.pack_start(model_preferences)
|
||||
def prompt_hold(self, *args):
|
||||
self.grab_focus()
|
||||
self.prompt.set_sensitive(False)
|
||||
self.send_button.set_visible(False)
|
||||
self.cancel_button.set_visible(True)
|
||||
|
||||
self.set_titlebar(header)
|
||||
def prompt_unhold(self, *args):
|
||||
self.prompt.set_sensitive(True)
|
||||
self.prompt.grab_focus()
|
||||
self.send_button.set_visible(True)
|
||||
self.cancel_button.set_visible(False)
|
||||
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
util.set_margin_all(vbox, 8)
|
||||
def chat_scroll_to_bottom(self):
|
||||
adj = self.chat_scroll.get_vadjustment()
|
||||
adj.set_value(adj.get_upper() - adj.get_page_size())
|
||||
|
||||
self.chat = ChatView(vexpand=True)
|
||||
vbox.append(self.chat)
|
||||
def chat_start_response(self):
|
||||
self._current_response = self.chat_add_message(sender=False)
|
||||
|
||||
self.prompt = PromptView(spacing=6)
|
||||
self.prompt.on_send = self.on_send
|
||||
self.prompt.on_cancel = self.on_cancel
|
||||
vbox.append(self.prompt)
|
||||
def chat_add_message(self, text="", sender=True):
|
||||
label = Gtk.Label(selectable=True, wrap=True, xalign=1.0 if sender else 0.0)
|
||||
label.set_text(text)
|
||||
util.set_margin_all(label, 8)
|
||||
|
||||
self.set_child(vbox)
|
||||
self.populate_models()
|
||||
row = Gtk.ListBoxRow()
|
||||
row.set_child(label)
|
||||
self.chat_list.append(row)
|
||||
|
||||
def populate_models(self):
|
||||
try:
|
||||
models = ollama.list()
|
||||
names = [m.model for m in models["models"]]
|
||||
except Exception as e:
|
||||
print ("Failed to fetch models: ", e)
|
||||
names = []
|
||||
self.chat_scroll_to_bottom()
|
||||
return label
|
||||
|
||||
self.model_store.splice(0, len(self.model_store), names)
|
||||
def chat_add_response_part(self, part):
|
||||
self._current_response.set_label(self._current_response.get_label() + part)
|
||||
self.chat_scroll_to_bottom()
|
||||
|
||||
def on_gpu_changed(self, value):
|
||||
self.get_application().num_gpu = value
|
||||
@Gtk.Template.Callback()
|
||||
def prompt_key_pressed(self, _a, keyval, _b, state):
|
||||
match keyval:
|
||||
case Gdk.KEY_Return if not state & Gdk.ModifierType.SHIFT_MASK:
|
||||
self.send_button_clicked(self.send_button)
|
||||
return True
|
||||
case _:
|
||||
return False
|
||||
|
||||
def on_preferences_clicked(self, button):
|
||||
self.model_prefs.popup()
|
||||
@Gtk.Template.Callback()
|
||||
def send_button_clicked(self, button):
|
||||
text = self.prompt_get_text()
|
||||
self.prompt_clear()
|
||||
|
||||
def on_send(self, *args):
|
||||
text = self.prompt.get_text()
|
||||
if not text:
|
||||
return
|
||||
|
||||
self.prompt.clear()
|
||||
self.chat.add_message(text, sender=True)
|
||||
self.chat.start_response()
|
||||
self.chat_add_message(text, sender=True)
|
||||
self.chat_start_response()
|
||||
|
||||
app = self.get_application()
|
||||
model = self.model_dropdown.get_selected_item().get_string()
|
||||
model = self.model_list.get_selected_item().get_string()
|
||||
|
||||
app.history_push(role="user", text=text)
|
||||
|
||||
options = {
|
||||
"num_gpu": self.gpu_layers_spin.get_value_as_int(),
|
||||
"low_vram": self.low_vram_switch.get_state()
|
||||
}
|
||||
|
||||
async def task():
|
||||
await app.send_prompt(model, text)
|
||||
await app.send_prompt(model, options)
|
||||
|
||||
if app.current_async_task and not app.current_async_task.done():
|
||||
app.current_async_task.cancel()
|
||||
|
||||
app.current_async_task = asyncio.run_coroutine_threadsafe(task(), app.async_loop)
|
||||
|
||||
def on_cancel(self, *args):
|
||||
@Gtk.Template.Callback()
|
||||
def cancel_button_clicked(self, *args):
|
||||
app = self.get_application()
|
||||
app.stop_generation()
|
||||
|
||||
@@ -91,6 +119,10 @@ class PyLlama(Gtk.Application):
|
||||
GLib.set_application_name("PyLlama")
|
||||
|
||||
self.num_gpu = 15
|
||||
self.history_size = 4
|
||||
self.history = []
|
||||
self.model_store = Gtk.StringList()
|
||||
self._populate_models()
|
||||
|
||||
self.client = ollama.AsyncClient()
|
||||
self.async_loop = asyncio.new_event_loop()
|
||||
@@ -106,21 +138,42 @@ class PyLlama(Gtk.Application):
|
||||
self.async_loop.run_forever()
|
||||
return True
|
||||
|
||||
def _populate_models(self):
|
||||
try:
|
||||
models = ollama.list()
|
||||
names = [m.model for m in models["models"]]
|
||||
except Exception as e:
|
||||
print ("Failed to fetch models: ", e)
|
||||
names = []
|
||||
|
||||
self.model_store.splice(0, len(self.model_store), names)
|
||||
|
||||
def stop_generation(self):
|
||||
if self.current_async_task and not self.current_async_task.done():
|
||||
self.current_async_task.cancel()
|
||||
|
||||
async def send_prompt(self, model, text=""):
|
||||
messages = [{ "role": "user", "content": text }]
|
||||
GLib.idle_add(self.window.prompt.hold, True)
|
||||
def history_push(self, role, text):
|
||||
self.history.append({"role": role, "content": text})
|
||||
while len(self.history) > self.history_size:
|
||||
self.history.pop(0)
|
||||
|
||||
async def send_prompt(self, model, options):
|
||||
GLib.idle_add(self.window.prompt_hold, True)
|
||||
try:
|
||||
async for part in await self.client.chat(model=model, messages=messages, stream=True, options={ "num_gpu": self.num_gpu }):
|
||||
GLib.idle_add(self.window.chat.add_response_part, part.message.content)
|
||||
messages = list(self.history)[-self.history_size:]
|
||||
async for part in await self.client.chat(model=model, messages=messages, stream=True, options=options):
|
||||
chunk = part.message.content
|
||||
|
||||
if not self.history or self.history[-1]["role"] != "assistant":
|
||||
self.history.append({"role": "assistant", "content": chunk})
|
||||
else:
|
||||
self.history[-1]["content"] += chunk
|
||||
|
||||
GLib.idle_add(self.window.chat_add_response_part, part.message.content)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
finally:
|
||||
GLib.idle_add(self.window.prompt.unhold, True)
|
||||
GLib.idle_add(self.window.prompt_unhold, True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = PyLlama()
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import gi
|
||||
from gi.repository import Gtk
|
||||
|
||||
import util
|
||||
|
||||
class ModelPreferenceView(Gtk.Popover):
|
||||
def __init__(self, on_gpu_change=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.on_gpu_change = on_gpu_change
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
util.set_margin_all(box, 8)
|
||||
|
||||
label = Gtk.Label(label="GPU Layers")
|
||||
box.append(label)
|
||||
|
||||
gpu_spin = Gtk.SpinButton(adjustment=Gtk.Adjustment(
|
||||
value=15, lower=0, upper=50, step_increment=1,
|
||||
), digits=0, hexpand=True)
|
||||
|
||||
gpu_spin.connect("value-changed", lambda spin: self.on_gpu_change(spin.get_value_as_int()))
|
||||
box.append(gpu_spin)
|
||||
|
||||
self.set_child(box)
|
||||
@@ -1,65 +0,0 @@
|
||||
import gi
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
class PromptView(Gtk.Box):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.prompt = Gtk.TextView(left_margin=12, top_margin=14,
|
||||
right_margin=12, bottom_margin=14)
|
||||
|
||||
prompt_scroll = Gtk.ScrolledWindow(hexpand=True)
|
||||
prompt_scroll.set_propagate_natural_height(True)
|
||||
prompt_scroll.set_max_content_height(150)
|
||||
prompt_scroll.set_child(self.prompt)
|
||||
|
||||
key_controller = Gtk.EventControllerKey()
|
||||
key_controller.connect("key-pressed", self.on_key_pressed)
|
||||
self.prompt.add_controller(key_controller)
|
||||
|
||||
self.prompt_button = Gtk.Button(icon_name="go-next-symbolic")
|
||||
self.btn_handler = self.prompt_button.connect("clicked", self.on_send)
|
||||
|
||||
self.append (prompt_scroll)
|
||||
self.append (self.prompt_button)
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
return self.prompt.get_buffer()
|
||||
|
||||
def clear(self):
|
||||
self.buffer.set_text("")
|
||||
|
||||
def get_text(self):
|
||||
return self.buffer.get_text(self.buffer.get_start_iter(),
|
||||
self.buffer.get_end_iter(), False).strip()
|
||||
|
||||
def hold(self, *args):
|
||||
self.grab_focus()
|
||||
self.prompt.set_sensitive(False)
|
||||
self.prompt_button.set_icon_name("process-stop-symbolic")
|
||||
self.prompt_button.disconnect(self.btn_handler)
|
||||
self.btn_handler = self.prompt_button.connect("clicked", self.on_cancel)
|
||||
|
||||
def unhold(self, *args):
|
||||
self.prompt.set_sensitive(True)
|
||||
self.prompt.grab_focus()
|
||||
self.prompt_button.set_icon_name("go-next-symbolic")
|
||||
self.prompt_button.disconnect(self.btn_handler)
|
||||
self.btn_handler = self.prompt_button.connect("clicked", self.on_send)
|
||||
|
||||
|
||||
def on_key_pressed(self, _a, keyval, _b, state):
|
||||
if keyval == Gdk.KEY_Return:
|
||||
if state & Gdk.ModifierType.SHIFT_MASK:
|
||||
return False
|
||||
self.on_send()
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_send(self, *args):
|
||||
pass
|
||||
|
||||
def on_cancel(self, *args):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
|
||||
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
|
||||
<!-- Created with Cambalache 0.99.3 -->
|
||||
<cambalache-project version="0.96.0" target_tk="gtk-4.0">
|
||||
<ui template-class="main_window" filename="pyllama.ui" sha256="37b3c167cb000ca8bb319efb8748b43b8bdde99fab64736fa1726c7f73e77327"/>
|
||||
</cambalache-project>
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.99.3 -->
|
||||
<interface>
|
||||
<!-- interface-name pyllama.ui -->
|
||||
<!-- interface-description The main window for the PyLlama interface program for ollama. -->
|
||||
<!-- interface-copyright PyLlama - a simple interface to ollama
|
||||
Copyright (C) 2026 Emil Kosz
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. -->
|
||||
<!-- interface-authors Emil Kosz -->
|
||||
<requires lib="gtk" version="4.20"/>
|
||||
<template class="main_window" parent="GtkApplicationWindow">
|
||||
<property name="accessible-role">main</property>
|
||||
<property name="decorated">True</property>
|
||||
<property name="default-height">600</property>
|
||||
<property name="default-width">800</property>
|
||||
<property name="show-menubar">False</property>
|
||||
<property name="titlebar">
|
||||
<object class="GtkHeaderBar">
|
||||
<child>
|
||||
<object class="GtkDropDown" id="model_list">
|
||||
<property name="css-name">dropdown</property>
|
||||
<property name="tooltip-text">Select model</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="active">False</property>
|
||||
<property name="icon-name">applications-engineering-symbolic</property>
|
||||
<property name="popover">
|
||||
<object class="GtkPopover" id="model_parameters">
|
||||
<property name="child">
|
||||
<object class="GtkGrid">
|
||||
<property name="column-homogeneous">False</property>
|
||||
<property name="column-spacing">12</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="row-homogeneous">False</property>
|
||||
<property name="row-spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">GPU layers</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="gpu_layers_spin">
|
||||
<property name="adjustment">
|
||||
<object class="GtkAdjustment">
|
||||
<property name="step-increment">1.0</property>
|
||||
<property name="upper">50.0</property>
|
||||
<property name="value">25.0</property>
|
||||
</object>
|
||||
</property>
|
||||
<property name="climb-rate">0.0</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="value">25.0</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Low VRAM</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="low_vram_switch">
|
||||
<property name="halign">end</property>
|
||||
<property name="hexpand">False</property>
|
||||
<property name="hexpand-set">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="baseline-child">-1</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="margin-bottom">8</property>
|
||||
<property name="margin-end">8</property>
|
||||
<property name="margin-start">8</property>
|
||||
<property name="margin-top">8</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="chat_scroll">
|
||||
<child>
|
||||
<object class="GtkListBox" id="chat_list">
|
||||
<property name="vexpand">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="max-content-height">200</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="prompt">
|
||||
<property name="bottom-margin">14</property>
|
||||
<property name="css-name">textview</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="left-margin">12</property>
|
||||
<property name="monospace">False</property>
|
||||
<property name="pixels-above-lines">0</property>
|
||||
<property name="pixels-below-lines">0</property>
|
||||
<property name="pixels-inside-wrap">0</property>
|
||||
<property name="right-margin">12</property>
|
||||
<property name="top-margin">14</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="halign">fill</property>
|
||||
<property name="hexpand">False</property>
|
||||
<property name="hexpand-set">False</property>
|
||||
<property name="icon-name">media-playback-start-symbolic</property>
|
||||
<signal name="clicked" handler="send_button_clicked"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="icon-name">media-playback-stop-symbolic</property>
|
||||
<property name="visible">False</property>
|
||||
<signal name="clicked" handler="cancel_button_clicked"/>
|
||||
<accessibility>
|
||||
<state name="hidden">True</state>
|
||||
</accessibility>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<object class="GtkEventControllerKey" id="prompt_controller">
|
||||
<signal name="key-pressed" handler="prompt_key_pressed"/>
|
||||
</object>
|
||||
</interface>
|
||||
Reference in New Issue
Block a user