55 Commits
1.0.6 ... 1.1.0

Author SHA1 Message Date
jeffser
9e2b55a249 Spanish update 2024-08-10 21:32:19 -06:00
jeffser
0fbb94cd72 Updated languages again 2024-08-10 21:32:10 -06:00
jeffser
004b3f8574 Preparing for 1.1.0 2024-08-10 21:26:33 -06:00
jeffser
7d1931dd17 Updated languages 2024-08-10 21:18:22 -06:00
jeffser
8b7f41afa7 Fixed codeblocks spacing 2024-08-10 21:10:23 -06:00
jeffser
4bc0832865 Better padding for focus border on messages 2024-08-10 21:03:19 -06:00
jeffser
a66c6d5f40 Fixed nasty clear chat glitch 2024-08-10 20:37:51 -06:00
jeffser
33b7cae24d Show date on stopped messages 2024-08-10 20:31:03 -06:00
jeffser
47f5c88ef2 Don't regenerate title when there's a custom chat title 2024-08-10 20:22:39 -06:00
jeffser
ffe382aee2 Added default buttons to dialogs 2024-08-10 20:14:51 -06:00
jeffser
919f71ee78 Fixed regenerate button 2024-08-10 20:05:41 -06:00
jeffser
404d4476ae Fixed import export 2024-08-10 19:27:03 -06:00
jeffser
f2b243cd5f Changed python 2 to python from codeblocks 2024-08-10 19:23:11 -06:00
Louis Chauvet-Villaret
c2fae41355 Complete french translation (#210)
Model description translated with ChatGPT, and it's work really well
2024-08-08 14:13:54 -06:00
jeffser
8fda2cde9e Fixed return + shift should make a new line 2024-08-08 10:56:45 -06:00
jeffser
930380cdce Fixed Hindi 2024-08-08 10:56:22 -06:00
jeffser
5b788ffe15 Missed a comma 2024-08-07 23:58:58 -06:00
jeffser
521c2bdde5 Fixed syntax 2024-08-07 23:57:23 -06:00
aritra saha
eee73b1218 some update and fix for welcome screen. (#206)
* Update bn.po

* Update hi.po

* Update window.py

* Update window.py

* Update window.py

* Update window.py

* Update window.py

I removed some and added commas

---------

Co-authored-by: Jeffry Samuel <69224322+Jeffser@users.noreply.github.com>
2024-08-07 23:54:34 -06:00
jeffser
87d6da26c9 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-07 21:37:39 -06:00
jeffser
2029cd5cd2 Added Hindi credits 2024-08-07 21:37:32 -06:00
Jeffry Samuel
36be752ee6 Update README.md 2024-08-07 21:34:48 -06:00
jeffser
5b3586789f Spanish Update 2024-08-07 21:32:57 -06:00
jeffser
6ce670e643 Updated translations 2024-08-07 21:27:08 -06:00
jeffser
dd70e8139c Give textview focus at launch 2024-08-07 21:21:26 -06:00
jeffser
3ac0936d1a Prevent enter key send when a response is being received 2024-08-07 21:20:03 -06:00
jeffser
1477bacf6a Removed 'Featured models' from welcome dialog, added 'Open Model Manager' button in welcome screen if the user doesn't have models 2024-08-07 21:02:29 -06:00
jeffser
d339a18901 Fixed toast message 2024-08-07 20:43:23 -06:00
jeffser
f9460416d9 Prevent regenerating message whilst receiving a response 2024-08-07 20:41:49 -06:00
jeffser
a9112cf3da Added welcome screen 2024-08-07 20:39:46 -06:00
jeffser
c873b49700 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-07 19:41:58 -06:00
jeffser
3c553e37d8 Fixed pull by name feature 2024-08-07 19:41:34 -06:00
aritra saha
0c47fbb1f7 update hi (#200)
* Update hi.po

* Update hi.po

* Update LINGUAS

* Update update_translation_files.sh

* Update hi.po

* Update hi.po

* Update hi.po
2024-08-07 19:35:18 -06:00
jeffser
476138ef53 Changed oars rating 2024-08-06 23:54:15 -06:00
jeffser
385ca4f0fa Added focus indicator for message being edited 2024-08-06 17:12:46 -06:00
jeffser
46fd642789 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-06 17:08:56 -06:00
jeffser
e48249c7c9 Focus indicators on messages 2024-08-06 17:08:50 -06:00
aritra saha
9e8535e97e Update bn.po (#198) 2024-08-06 14:42:40 -06:00
jeffser
a794c63a5a Fixed url 2024-08-06 13:49:22 -06:00
ProjectMoon
f3610a46a2 Fix bearer token use for remote ollama APIs. (#197) 2024-08-06 13:48:05 -06:00
aritra saha
20fd2cf6e3 hi updTe (#195)
* Update bn.po

* Update hi.po

* Update hi.po
2024-08-06 13:40:24 -06:00
jeffser
7bf345d09d Updated language files 2024-08-06 13:39:54 -06:00
jeffser
17e9560449 Fixed (none) in model selector problem 2024-08-06 13:15:15 -06:00
jeffser
c02e6a565e New model selector design + moved around the delete chat option on menus 2024-08-06 12:59:41 -06:00
jeffser
7fbc9b9bde Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-05 23:12:29 -06:00
jeffser
416e97d488 Remade model selector 2024-08-05 23:12:22 -06:00
aritra saha
753060d9f3 another small fix (#194)
* Update bn.po

* Update hi.po
2024-08-05 17:52:38 -06:00
jeffser
972c53000c Changed splitview collapsing behaviour 2024-08-05 17:46:01 -06:00
jeffser
2b948a49a0 Made model manager navigatable by keyboard 2024-08-05 16:45:26 -06:00
jeffser
7999548738 Added fixes for accessibility / screen reader fixes (tested on Orca) 2024-08-05 16:07:49 -06:00
jeffser
d4d13b793f Fixed width of dialogs 2024-08-05 13:59:11 -06:00
jeffser
210b6f0d89 Removed search from model dropdown 2024-08-05 13:51:31 -06:00
jeffser
7f5894b274 Added delete chat option in secondary menu 2024-08-05 13:50:42 -06:00
jeffser
2dc24ab945 Made manage models dialog appear faster 2024-08-05 13:45:18 -06:00
jeffser
2f153c9974 Added ctrl+q shortcut to close app 2024-08-05 13:45:02 -06:00
21 changed files with 6893 additions and 5076 deletions

View File

@@ -46,6 +46,7 @@ Language | Contributors
🇳🇴 Norwegian | [CounterFlow64](https://github.com/CounterFlow64)
🇮🇳 Bengali | [Aritra Saha](https://github.com/olumolu)
🇨🇳 Simplified Chinese | [Yuehao Sui](https://github.com/8ar10der) , [Aleksana](https://github.com/Aleksanaa)
🇮🇳 Hindi | [Aritra Saha](https://github.com/olumolu)
---

View File

@@ -70,9 +70,7 @@
<caption>Multiple models being downloaded</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="money-purchasing">mild</content_attribute>
</content_rating>
<content_rating type="oars-1.1" />
<url type="bugtracker">https://github.com/Jeffser/Alpaca/issues</url>
<url type="homepage">https://jeffser.com/alpaca/</url>
<url type="donation">https://github.com/sponsors/Jeffser</url>
@@ -80,6 +78,37 @@
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
<releases>
<release version="1.1.0" date="2024-08-10">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.1.0</url>
<description>
<p>New</p>
<ul>
<li>Model manager opens faster</li>
<li>Delete chat option in secondary menu</li>
<li>New model selector popup</li>
<li>Standard shortcuts</li>
<li>Model manager is navigable with keyboard</li>
<li>Changed sidebar collapsing behavior</li>
<li>Focus indicators on messages</li>
<li>Welcome screen</li>
<li>Give message entry focus at launch</li>
<li>Generally better code</li>
</ul>
<p>Fixes</p>
<ul>
<li>Better width for dialogs</li>
<li>Better compatibility with screen readers</li>
<li>Fixed message regenerator</li>
<li>Removed 'Featured models' from welcome dialog</li>
<li>Added default buttons to dialogs</li>
<li>Fixed import / export of chats</li>
<li>Changed Python2 title to Python on code blocks</li>
<li>Prevent regeneration of title when the user changed it to a custom title</li>
<li>Show date on stopped messages</li>
<li>Fix clear chat error</li>
</ul>
</description>
</release>
<release version="1.0.6" date="2024-08-04">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.6</url>
<description>

View File

@@ -1,5 +1,5 @@
project('Alpaca', 'c',
version: '1.0.6',
version: '1.1.0',
meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ],
)

View File

@@ -4,4 +4,5 @@ pt_BR
fr
nb_NO
bn
zh_CN
zh_CN
hi

File diff suppressed because it is too large Load Diff

1028
po/bn.po

File diff suppressed because it is too large Load Diff

1078
po/es.po

File diff suppressed because it is too large Load Diff

1971
po/fr.po

File diff suppressed because it is too large Load Diff

2110
po/hi.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

979
po/ru.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@
<file alias="icons/scalable/status/edit-symbolic.svg">icons/edit-symbolic.svg</file>
<file alias="icons/scalable/status/image-missing-symbolic.svg">icons/image-missing-symbolic.svg</file>
<file alias="icons/scalable/status/update-symbolic.svg">icons/update-symbolic.svg</file>
<file alias="icons/scalable/status/down-symbolic.svg">icons/down-symbolic.svg</file>
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>

View File

@@ -28,6 +28,7 @@ def clear_chat(self):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("clear", _("Clear"))
dialog.set_response_appearance("clear", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("clear")
dialog.choose(
parent = self,
cancellable = None,
@@ -49,6 +50,7 @@ def delete_chat(self, chat_name):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("delete", _("Delete"))
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("delete")
dialog.choose(
parent = self,
cancellable = None,
@@ -74,10 +76,10 @@ def rename_chat(self, chat_name, label_element):
extra_child=entry,
close_response="cancel"
)
entry.connect("activate", lambda dialog, old_chat_name=chat_name, entry=entry, label_element=label_element: rename_chat_response(self, dialog, None, old_chat_name, entry, label_element))
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("rename", _("Rename"))
dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("rename")
dialog.choose(
parent = self,
cancellable = None,
@@ -102,10 +104,10 @@ def new_chat(self):
extra_child=entry,
close_response="cancel"
)
entry.connect("activate", lambda dialog, entry: new_chat_response(self, dialog, None, entry))
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("create", _("Create"))
dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("create")
dialog.choose(
parent = self,
cancellable = None,
@@ -128,6 +130,7 @@ def stop_pull_model(self, model_name):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("stop", _("Stop"))
dialog.set_response_appearance("stop", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("stop")
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
@@ -149,6 +152,7 @@ def delete_model(self, model_name):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("delete", _("Delete"))
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("delete")
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
@@ -171,6 +175,7 @@ def remove_attached_file(self, name):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("remove", _("Remove"))
dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("remove")
dialog.choose(
parent = self,
cancellable = None,
@@ -210,11 +215,11 @@ def reconnect_remote(self, current_url, current_bearer_token):
body=_("The remote instance has disconnected"),
extra_child=container
)
#entry.connect("activate", lambda entry, dialog: reconnect_remote_response(self, dialog, None, entry))
dialog.add_response("close", _("Close Alpaca"))
dialog.add_response("local", _("Use local instance"))
dialog.add_response("remote", _("Connect"))
dialog.set_response_appearance("remote", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("remote")
dialog.choose(
parent = self,
cancellable = None,
@@ -243,6 +248,7 @@ def create_model_from_existing(self):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
@@ -271,7 +277,7 @@ def create_model_from_name_response(self, dialog, task, entry):
def create_model_from_name(self):
entry = Gtk.Entry()
entry.get_delegate().connect("insert-text", self.check_alphanumeric)
entry.get_delegate().connect("insert-text", lambda *_ : self.check_alphanumeric(*_, ['-', '.', ':', '_', '/']))
dialog = Adw.AlertDialog(
heading=_("Pull Model"),
body=_("Input the name of the model in this format\nname:tag"),
@@ -280,6 +286,7 @@ def create_model_from_name(self):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
@@ -355,6 +362,7 @@ def youtube_caption(self, video_url):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
@@ -393,6 +401,7 @@ def attach_website(self, url):
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2.292969 6.707031 l 5 5 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 l 5 -5 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 s -1.023437 -0.390625 -1.414062 0 l -4.292969 4.292969 l -4.292969 -4.292969 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 s -0.390625 1.023437 0 1.414062 z m 0 0" fill="#222222" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -43,7 +43,8 @@ translators = [
'CounterFlow64 (Norwegian) https://github.com/CounterFlow64',
'Aritra Saha (Bengali) https://github.com/olumolu',
'Yuehao Sui (Simplified Chinese) https://github.com/8ar10der',
'Aleksana (Simplified Chinese) https://github.com/Aleksanaa'
'Aleksana (Simplified Chinese) https://github.com/Aleksanaa',
'Aritra Saha (Hindi) https://github.com/olumolu'
]
class AlpacaApplication(Adw.Application):
@@ -52,7 +53,7 @@ class AlpacaApplication(Adw.Application):
def __init__(self, version):
super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>w'])
self.create_action('quit', lambda *_: self.quit(), ['<primary>w', '<primary>q'])
self.create_action('preferences', lambda *_: AlpacaWindow.show_preferences_dialog(self.props.active_window), ['<primary>comma'])
self.create_action('about', self.on_about_action)
self.version = version

View File

@@ -1,7 +1,3 @@
.message_input_scroll_window > * {
box-shadow: none;
border-width: 0;
}
.message_text_view, .modelfile_textview {
background-color: rgba(0,0,0,0);
}
@@ -12,3 +8,19 @@
border-radius: 5px;
padding: 5px;
}
.model_list_box {
padding: 0;
}
.manage_models_button {
padding: 6px 8px 6px 8px;
}
.model_list_box > * {
margin: 0;
}
.user_message, .response_message {
padding: 12px;
border-radius: 10px;
}
.user_message:focus, .response_message:focus, .editing_message_textview:focus, .code_block:focus {
box-shadow: 0 0 1px 2px mix(@accent_color, @window_bg_color, 0.5);
}

View File

@@ -19,7 +19,7 @@
"""
Handles the main window
"""
import json, threading, os, re, base64, sys, gettext, uuid, shutil, tarfile, tempfile, logging
import json, threading, os, re, base64, sys, gettext, uuid, shutil, tarfile, tempfile, logging, random
from io import BytesIO
from PIL import Image
from pypdf import PdfReader
@@ -64,6 +64,30 @@ class AlpacaWindow(Adw.ApplicationWindow):
pulling_models = {}
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []}
attachments = {}
possible_prompts = [
"What can you do?",
"Give me a pancake recipe",
"Why is the sky blue?",
"Can you tell me a joke?",
"Give me a healthy breakfast recipe",
"How to make a pizza",
"Can you write a poem?",
"Can you write a story?",
"What is GNU-Linux?",
"Which is the best Linux distro?",
"Why is Pluto not a planet?",
"What is a black-hole?",
"Tell me how to stay fit",
"Write a conversation between sun and Earth",
"Why is the grass green?",
"Write an Haïku about AI",
"What is the meaning of life?",
"Explain quantum physics in simple terms",
"Explain the theory of relativity",
"Explain how photosynthesis works",
"Recommend a film about nature",
"What is nostalgia?"
]
#Override elements
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
@@ -107,8 +131,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
file_filter_gguf = Gtk.Template.Child()
file_filter_attachments = Gtk.Template.Child()
attachment_button = Gtk.Template.Child()
model_drop_down = Gtk.Template.Child()
model_string_list = Gtk.Template.Child()
chat_right_click_menu = Gtk.Template.Child()
model_tag_list_box = Gtk.Template.Child()
navigation_view_manage_models = Gtk.Template.Child()
@@ -118,6 +140,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
model_searchbar = Gtk.Template.Child()
no_results_page = Gtk.Template.Child()
model_link_button = Gtk.Template.Child()
model_list_box = Gtk.Template.Child()
model_popover = Gtk.Template.Child()
model_selector_button = Gtk.Template.Child()
chat_welcome_screen : Adw.StatusPage = None
manage_models_dialog = Gtk.Template.Child()
pulling_model_list_box = Gtk.Template.Child()
@@ -136,32 +162,22 @@ class AlpacaWindow(Adw.ApplicationWindow):
style_manager = Adw.StyleManager()
@Gtk.Template.Callback()
def verify_if_image_can_be_used(self, pspec=None, user_data=None):
logger.debug("Verifying if image can be used")
if self.model_drop_down.get_selected_item() == None:
return True
selected = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1).split(":")[0]
if selected in [key for key, value in self.available_models.items() if value["image"]]:
for name, content in self.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat"])
return True
for name, content in self.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat", "error"])
return False
@Gtk.Template.Callback()
def stop_message(self, button=None):
if self.loading_spinner:
self.chat_container.remove(self.loading_spinner)
message_id = list(self.chats["chats"][self.chats["selected_chat"]]["messages"])[-1]
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["date"], '%Y/%m/%d %H:%M:%S')))
text = f"\n\n{self.convert_model_name(self.chats['chats'][self.chats['selected_chat']]['messages'][message_id]['model'], 0)}\n<small>{formated_date}</small>"
self.bot_message.insert_markup(self.bot_message.get_end_iter(), text, len(text.encode('utf-8')))
self.add_code_blocks()
self.toggle_ui_sensitive(True)
self.switch_send_stop_button()
self.switch_send_stop_button(True)
self.bot_message = None
self.bot_message_box = None
self.bot_message_view = None
self.bot_message_button_container = None
self.save_history()
@Gtk.Template.Callback()
def send_message(self, button=None):
@@ -178,8 +194,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.editing_message = None
self.save_history()
self.show_toast(_("Message edited successfully"), self.main_overlay)
if self.bot_message or self.get_focus() not in (self.message_text_view, self.send_button):
if button and not button.get_visible():
return
if not self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False):
return
@@ -191,7 +206,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats['order'].remove(self.chats['selected_chat'])
self.chats['order'].insert(0, self.chats['selected_chat'])
self.save_history()
current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
current_model = self.get_current_model(1)
if current_model is None:
self.show_toast(_("Please select a model before chatting"), self.main_overlay)
return
@@ -232,7 +247,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
"keep_alive": f"{self.model_tweaks['keep_alive']}m"
}
self.switch_send_stop_button()
self.switch_send_stop_button(False)
self.toggle_ui_sensitive(False)
#self.attachments[name] = {"path": file_path, "type": file_type, "content": content}
@@ -245,6 +260,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
bot_id=self.generate_uuid()
self.show_message("", True, message_id=bot_id)
if self.chat_welcome_screen:
self.chat_container.remove(self.chat_welcome_screen)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], bot_id))
thread.start()
if len(data['messages']) == 1:
@@ -286,13 +304,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
if row and row.get_child().get_name() != self.chats["selected_chat"]:
self.chats["selected_chat"] = row.get_child().get_name()
self.load_history_into_chat()
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"]) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
last_model_used = self.convert_model_name(last_model_used, 0)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i)
for i, m in enumerate(self.local_models):
if m == last_model_used:
self.model_list_box.select_row(self.model_list_box.get_row_at_index(i))
break
else:
self.load_history_into_chat()
self.save_history()
@Gtk.Template.Callback()
@@ -303,7 +322,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.remote_url = entry.get_text()
logger.debug(f"Changing remote url: {self.remote_url}")
if self.run_remote:
connection_handler.url = self.remote_url
connection_handler.URL = self.remote_url
if self.verify_connection() == False:
entry.set_css_classes(["error"])
self.show_toast(_("Failed to connect to server"), self.preferences_dialog)
@@ -314,23 +333,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.save_server_config()
return
if self.remote_url and self.run_remote:
connection_handler.url = self.remote_url
connection_handler.URL = self.remote_url
if self.verify_connection() == False:
entry.set_css_classes(["error"])
self.show_toast(_("Failed to connect to server"), self.preferences_dialog)
@Gtk.Template.Callback()
def pull_featured_model(self, button):
action_row = button.get_parent().get_parent().get_parent()
button.get_parent().remove(button)
model = f"{action_row.get_title().lower()}:latest"
action_row.set_subtitle(_("Pulling in the background..."))
spinner = Gtk.Spinner()
spinner.set_spinning(True)
action_row.add_suffix(spinner)
action_row.set_sensitive(False)
self.pull_model(model)
@Gtk.Template.Callback()
def closing_app(self, user_data):
if self.get_hide_on_close():
@@ -427,26 +434,78 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.available_model_list_box.set_visible(True)
self.no_results_page.set_visible(False)
def manage_models_button_activate(self, button=None):
logger.debug(f"Managing models")
self.update_list_local_models()
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
last_model_used = self.convert_model_name(last_model_used, 0)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i)
break
self.manage_models_dialog.present(self)
@Gtk.Template.Callback()
def close_model_popup(self, *_):
self.model_popover.hide()
@Gtk.Template.Callback()
def change_model(self, listbox=None, row=None):
if not row:
current_model = self.model_selector_button.get_name()
if current_model != 'NO_MODEL':
for i, m in enumerate(self.local_models):
if m == current_model:
self.model_list_box.select_row(self.model_list_box.get_row_at_index(i))
return
if len(self.local_models) > 0:
self.model_list_box.select_row(self.model_list_box.get_row_at_index(0))
return
else:
model_name = None
else:
model_name = row.get_child().get_label()
button_content = Gtk.Box(
spacing=10
)
button_content.append(
Gtk.Label(
label=model_name if model_name else _("Select a Model"),
ellipsize=2
)
)
button_content.append(
Gtk.Image.new_from_icon_name("down-symbolic")
)
self.model_selector_button.set_name(row.get_name() if row else 'NO_MODEL')
self.model_selector_button.set_child(button_content)
self.close_model_popup()
self.verify_if_image_can_be_used()
def verify_if_image_can_be_used(self):
logger.debug("Verifying if image can be used")
selected = self.get_current_model(1)
if selected == None:
return True
selected = selected.split(":")[0]
if selected in [key for key, value in self.available_models.items() if value["image"]]:
for name, content in self.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat"])
return True
for name, content in self.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat", "error"])
return False
def convert_model_name(self, name:str, mode:int) -> str: # mode=0 name:tag -> Name (tag) | mode=1 Name (tag) -> name:tag
if mode == 0:
return "{} ({})".format(name.split(":")[0].replace("-", " ").title(), name.split(":")[1])
if mode == 1:
return "{}:{}".format(name.split(" (")[0].replace(" ", "-").lower(), name.split(" (")[1][:-1])
try:
if mode == 0:
return "{} ({})".format(name.split(":")[0].replace("-", " ").title(), name.split(":")[1])
if mode == 1:
return "{}:{}".format(name.split(" (")[0].replace(" ", "-").lower(), name.split(" (")[1][:-1])
except Exception as e:
pass
def check_alphanumeric(self, editable, text, length, position):
new_text = ''.join([char for char in text if char.isalnum() or char in ['-', '.', ':', '_']])
def get_current_model(self, mode:int) -> str:
if not self.model_list_box.get_selected_row():
return None
if mode == 0:
return self.model_list_box.get_selected_row().get_child().get_label()
if mode == 1:
return self.model_list_box.get_selected_row().get_name()
def check_alphanumeric(self, editable, text, length, position, allowed_chars):
new_text = ''.join([char for char in text if char.isalnum() or char in allowed_chars])
if new_text != text:
editable.stop_emission_by_name("insert-text")
@@ -455,7 +514,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter())
self.create_model_system.set_text('')
if not file:
response = connection_handler.simple_post(f"{connection_handler.url}/api/show", json.dumps({"name": self.convert_model_name(model, 1)}))
response = connection_handler.simple_post(f"{connection_handler.URL}/api/show", json.dumps({"name": self.convert_model_name(model, 1)}))
if response.status_code == 200:
data = json.loads(response.text)
modelfile = []
@@ -500,6 +559,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
if os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id)):
shutil.rmtree(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id))
self.save_history()
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"]) == 0:
self.load_history_into_chat()
def copy_message(self, message_element):
logger.debug("Copying message")
@@ -558,7 +619,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.file_preview_text_view.set_visible(True)
buffer = self.file_preview_text_view.get_buffer()
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), content, len(content))
buffer.insert(buffer.get_start_iter(), content, len(content.encode('utf-8')))
if file_type == 'youtube':
self.file_preview_dialog.set_title(content.split('\n')[0])
self.file_preview_open_button.set_name(content.split('\n')[2])
@@ -593,6 +654,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
return messages
def generate_chat_title(self, message, label_element):
if not label_element.get_name().startswith(_("New Chat")):
return
logger.debug("Generating chat title")
prompt = f"""
Generate a title following these rules:
@@ -605,11 +668,11 @@ Generate a title following these rules:
```PROMPT
{message['content']}
```"""
current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
current_model = self.get_current_model(1)
data = {"model": current_model, "prompt": prompt, "stream": False}
if 'images' in message:
data["images"] = message['images']
response = connection_handler.simple_post(f"{connection_handler.url}/api/generate", data=json.dumps(data))
response = connection_handler.simple_post(f"{connection_handler.URL}/api/generate", data=json.dumps(data))
new_chat_name = json.loads(response.text)["response"].strip().removeprefix("Title: ").removeprefix("title: ").strip('\'"').replace('\n', ' ').title().replace('\'S', '\'s')
new_chat_name = new_chat_name[:50] + (new_chat_name[50:] and '...')
@@ -620,14 +683,11 @@ Generate a title following these rules:
editable=False,
focusable=True,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12,
hexpand=True,
cursor_visible=False,
css_classes=["flat"],
css_classes=["flat", "response_message"] if bot else ["flat", "user_message"],
)
if not bot:
message_text.update_property([4, 7, 1], [_("User message"), True, msg])
message_buffer = message_text.get_buffer()
message_buffer.insert(message_buffer.get_end_iter(), msg)
if footer is not None:
@@ -666,7 +726,8 @@ Generate a title following these rules:
message_box = Gtk.Box(
orientation=1,
halign='fill',
css_classes=[None if bot else "card"]
css_classes=[None if bot else "card"],
spacing=5
)
message_text.set_valign(Gtk.Align.CENTER)
@@ -696,6 +757,7 @@ Generate a title following these rules:
name=os.path.join(self.data_dir, "chats", "{selected_chat}", message_id, image),
tooltip_text=_("Image")
)
image_element.update_property([4], [_("Image")])
button.connect("clicked", lambda button, file_path=path: self.preview_file(file_path, 'image', None))
except Exception as e:
logger.error(e)
@@ -722,6 +784,7 @@ Generate a title following these rules:
css_classes=["flat", "chat_image_button"],
tooltip_text=_("Missing Image")
)
image_texture.update_property([4], [_("Missing image")])
button.connect("clicked", lambda button : self.show_toast(_("Missing image"), self.main_overlay))
image_container.append(button)
message_box.append(image_scroller)
@@ -784,9 +847,8 @@ Generate a title following these rules:
def update_list_local_models(self):
logger.debug("Updating list of local models")
self.local_models = []
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
for i in range(self.model_string_list.get_n_items() -1, -1, -1):
self.model_string_list.remove(i)
response = connection_handler.simple_get(f"{connection_handler.URL}/api/tags")
self.model_list_box.remove_all()
if response.status_code == 200:
self.local_model_list_box.remove_all()
if len(json.loads(response.text)['models']) == 0:
@@ -810,9 +872,17 @@ Generate a title following these rules:
model_row.add_suffix(button)
self.local_model_list_box.append(model_row)
self.model_string_list.append(model_name)
selector_row = Gtk.ListBoxRow(
child = Gtk.Label(
label=model_name, halign=1, hexpand=True
),
halign=0,
hexpand=True,
name=model["name"],
tooltip_text=model_name
)
self.model_list_box.append(selector_row)
self.local_models.append(model["name"])
#self.verify_if_image_can_be_used()
else:
self.connection_error()
@@ -822,7 +892,7 @@ Generate a title following these rules:
def verify_connection(self):
try:
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
response = connection_handler.simple_get(f"{connection_handler.URL}/api/tags")
if response.status_code == 200:
self.save_server_config()
self.update_list_local_models()
@@ -845,7 +915,7 @@ Generate a title following these rules:
parts.append({"type": "normal", "text": normal_text.strip()})
language = match.group(1)
code_text = match.group(2)
parts.append({"type": "code", "text": code_text, "language": language})
parts.append({"type": "code", "text": code_text, "language": 'python3' if language == 'python' else language})
pos = end
# Match code blocks without language
no_lang_code_block_pattern = re.compile(r'`\n(.*?)\n`', re.DOTALL)
@@ -883,10 +953,8 @@ Generate a title following these rules:
editable=False,
focusable=True,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
hexpand=True,
css_classes=["flat"]
css_classes=["flat", "response_message"]
)
message_buffer = message_text.get_buffer()
@@ -915,6 +983,7 @@ Generate a title following these rules:
if footer: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
message_text.update_property([4, 7, 1], [_("Response message"), False, message_buffer.get_text(message_buffer.get_start_iter(), message_buffer.get_end_iter(), False)])
self.bot_message_box.append(message_text)
elif part['type'] == 'code':
language = None
@@ -932,10 +1001,11 @@ Generate a title following these rules:
buffer.set_style_scheme(source_style)
source_view = GtkSource.View(
auto_indent=True, indent_width=4, buffer=buffer, show_line_numbers=True,
top_margin=6, bottom_margin=6, left_margin=12, right_margin=12
top_margin=6, bottom_margin=6, left_margin=12, right_margin=12, css_classes=["code_block"]
)
source_view.update_property([4], [_("{}Code Block").format('{} '.format(language.get_name()) if language else "")])
source_view.set_editable(False)
code_block_box = Gtk.Box(css_classes=["card"], orientation=1, overflow=1)
code_block_box = Gtk.Box(css_classes=["card", "code_block"], orientation=1, overflow=1)
title_box = Gtk.Box(margin_start=12, margin_top=3, margin_bottom=3, margin_end=3)
title_box.append(Gtk.Label(label=language.get_name() if language else _("Code Block"), hexpand=True, xalign=0))
copy_button = Gtk.Button(icon_name="edit-copy-symbolic", css_classes=["flat", "circular"], tooltip_text=_("Copy Message"))
@@ -982,7 +1052,6 @@ Generate a title following these rules:
def update_bot_message(self, data, message_id):
if self.bot_message is None:
self.save_history()
sys.exit()
vadjustment = self.chat_window.get_vadjustment()
if message_id not in self.chats["chats"][self.chats["selected_chat"]]["messages"] or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
@@ -1007,9 +1076,9 @@ Generate a title following these rules:
for element in [self.chat_list_box, self.add_chat_button, self.secondary_menu_button]:
element.set_sensitive(status)
def switch_send_stop_button(self):
self.stop_button.set_visible(self.send_button.get_visible())
self.send_button.set_visible(not self.send_button.get_visible())
def switch_send_stop_button(self, send:bool):
self.stop_button.set_visible(not send)
self.send_button.set_visible(send)
def run_message(self, messages, model, message_id):
logger.debug("Running message")
@@ -1023,7 +1092,7 @@ Generate a title following these rules:
if self.regenerate_button:
GLib.idle_add(self.chat_container.remove, self.regenerate_button)
try:
response = connection_handler.stream_post(f"{connection_handler.url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=lambda data, message_id=message_id: self.update_bot_message(data, message_id))
response = connection_handler.stream_post(f"{connection_handler.URL}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=lambda data, message_id=message_id: self.update_bot_message(data, message_id))
if response.status_code != 200:
raise Exception('Network Error')
GLib.idle_add(self.add_code_blocks)
@@ -1040,40 +1109,44 @@ Generate a title following these rules:
GLib.idle_add(self.chat_container.append, self.regenerate_button)
self.regenerate_button.connect('clicked', lambda button, message_id=message_id, bot_message_box=self.bot_message_box, bot_message_button_container=self.bot_message_button_container : self.regenerate_message(message_id, bot_message_box, bot_message_button_container))
finally:
GLib.idle_add(self.switch_send_stop_button)
GLib.idle_add(self.switch_send_stop_button, True)
GLib.idle_add(self.toggle_ui_sensitive, True)
if self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
def regenerate_message(self, message_id, bot_message_box, bot_message_button_container):
self.bot_message_button_container = bot_message_button_container
self.bot_message_view = Gtk.TextView(
editable=False,
focusable=True,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
hexpand=True,
css_classes=["flat"]
)
self.bot_message = self.bot_message_view.get_buffer()
for widget in list(bot_message_box):
bot_message_box.remove(widget)
bot_message_box.append(self.bot_message_view)
history = self.convert_history_to_ollama()[:list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()).index(message_id)]
if message_id in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
del self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]
data = {
"model": self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1),
"messages": history,
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
"keep_alive": f"{self.model_tweaks['keep_alive']}m"
}
self.switch_send_stop_button()
self.toggle_ui_sensitive(False)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], message_id))
thread.start()
if not self.bot_message:
self.bot_message_button_container = bot_message_button_container
self.bot_message_view = Gtk.TextView(
editable=False,
focusable=True,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
hexpand=True,
css_classes=["flat"]
)
self.bot_message = self.bot_message_view.get_buffer()
self.bot_message_box = bot_message_box
for widget in list(bot_message_box):
bot_message_box.remove(widget)
bot_message_box.append(self.bot_message_view)
history = self.convert_history_to_ollama()[:list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()).index(message_id)]
if message_id in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
del self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]
data = {
"model": self.get_current_model(1),
"messages": history,
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
"keep_alive": f"{self.model_tweaks['keep_alive']}m"
}
self.switch_send_stop_button(False)
self.toggle_ui_sensitive(False)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], message_id))
thread.start()
else:
self.show_toast(_("Message cannot be regenerated while receiving a response"), self.main_overlay)
def pull_model_update(self, data, model_name):
if 'error' in data:
@@ -1093,11 +1166,12 @@ Generate a title following these rules:
def pull_model_process(self, model, modelfile):
if modelfile:
data = {"name": model, "modelfile": modelfile}
response = connection_handler.stream_post(f"{connection_handler.url}/api/create", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
response = connection_handler.stream_post(f"{connection_handler.URL}/api/create", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
else:
data = {"name": model}
response = connection_handler.stream_post(f"{connection_handler.url}/api/pull", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
response = connection_handler.stream_post(f"{connection_handler.URL}/api/pull", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
GLib.idle_add(self.update_list_local_models)
GLib.idle_add(self.change_model)
if response.status_code == 200 and 'error' not in self.pulling_models[model]:
GLib.idle_add(self.show_notification, _("Task Complete"), _("Model '{}' pulled successfully.").format(model), Gio.ThemedIcon.new("emblem-ok-symbolic"))
@@ -1164,7 +1238,6 @@ Generate a title following these rules:
self.model_link_button.set_name(self.available_models[model_name]['url'])
self.model_link_button.set_tooltip_text(self.available_models[model_name]['url'])
self.available_model_list_box.unselect_all()
self.model_tag_list_box.connect('row_selected', lambda list_box, row: self.confirm_pull_model(row.get_name()) if row else None)
self.model_tag_list_box.remove_all()
tags = self.available_models[model_name]['tags']
for tag_data in tags:
@@ -1174,12 +1247,24 @@ Generate a title following these rules:
subtitle = tag_data[1],
name = f"{model_name}:{tag_data[0]}"
)
tag_row.add_suffix(Gtk.Image.new_from_icon_name("folder-download-symbolic"))
download_icon = Gtk.Image.new_from_icon_name("folder-download-symbolic")
tag_row.add_suffix(download_icon)
download_icon.update_property([4], [_("Download {}:{}").format(model_name, tag_data[0])])
gesture_click = Gtk.GestureClick.new()
gesture_click.connect("pressed", lambda *_, name=f"{model_name}:{tag_data[0]}" : self.confirm_pull_model(name))
event_controller_key = Gtk.EventControllerKey.new()
event_controller_key.connect("key-pressed", lambda controller, key, *_, name=f"{model_name}:{tag_data[0]}" : self.confirm_pull_model(name) if key in (Gdk.KEY_space, Gdk.KEY_Return) else None)
tag_row.add_controller(gesture_click)
tag_row.add_controller(event_controller_key)
self.model_tag_list_box.append(tag_row)
return True
def update_list_available_models(self):
logger.debug("Updating list of available models")
self.available_model_list_box.connect('row_selected', lambda list_box, row: self.list_available_model_tags(row.get_name()) if row else None)
self.available_model_list_box.remove_all()
for name, model_info in self.available_models.items():
model = Adw.ActionRow(
@@ -1187,13 +1272,19 @@ Generate a title following these rules:
subtitle = available_models_descriptions.descriptions[name] + ("\n\n<b>{}</b>".format(_("Image Recognition")) if model_info['image'] else ""),
name = name
)
if model_info["image"]:
image_icon = Gtk.Image.new_from_icon_name("image-x-generic-symbolic")
image_icon.set_margin_start(5)
#model.add_suffix(image_icon)
next_icon = Gtk.Image.new_from_icon_name("go-next")
next_icon.set_margin_start(5)
next_icon.update_property([4], [_("Enter download menu for {}").format(name.replace("-", ""))])
model.add_suffix(next_icon)
gesture_click = Gtk.GestureClick.new()
gesture_click.connect("pressed", lambda *_, name=name : self.list_available_model_tags(name))
event_controller_key = Gtk.EventControllerKey.new()
event_controller_key.connect("key-pressed", lambda controller, key, *_, name=name : self.list_available_model_tags(name) if key in (Gdk.KEY_space, Gdk.KEY_Return) else None)
model.add_controller(gesture_click)
model.add_controller(event_controller_key)
self.available_model_list_box.append(model)
def save_history(self):
@@ -1201,17 +1292,56 @@ Generate a title following these rules:
with open(os.path.join(self.data_dir, "chats", "chats.json"), "w+", encoding="utf-8") as f:
json.dump(self.chats, f, indent=4)
def send_sample_prompt(self, prompt):
buffer = self.message_text_view.get_buffer()
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), prompt, len(prompt.encode('utf-8')))
self.send_message()
def load_history_into_chat(self):
for widget in list(self.chat_container): self.chat_container.remove(widget)
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
if message:
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(message['date'] + (":00" if message['date'].count(":") == 1 else ""), '%Y/%m/%d %H:%M:%S')))
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{formated_date}</small>", message['images'] if 'images' in message else None, message['files'] if 'files' in message else None, message_id=key)
else:
self.show_message(message['content'], True, f"\n\n{self.convert_model_name(message['model'], 0)}\n<small>{formated_date}</small>", message_id=key)
self.add_code_blocks()
self.bot_message = None
self.chat_welcome_screen = None
if len(self.chats['chats'][self.chats["selected_chat"]]['messages']) > 0:
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
if message:
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(message['date'] + (":00" if message['date'].count(":") == 1 else ""), '%Y/%m/%d %H:%M:%S')))
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{formated_date}</small>", message['images'] if 'images' in message else None, message['files'] if 'files' in message else None, message_id=key)
else:
self.show_message(message['content'], True, f"\n\n{self.convert_model_name(message['model'], 0)}\n<small>{formated_date}</small>", message_id=key)
self.add_code_blocks()
self.bot_message = None
else:
button_container = Gtk.Box(
orientation = 1,
spacing = 10,
halign = 3
)
if len(self.local_models) > 0:
for prompt in random.sample(self.possible_prompts, 3):
prompt_button = Gtk.Button(
label=prompt,
tooltip_text=_("Send prompt: '{}'").format(prompt)
)
prompt_button.connect('clicked', lambda *_, prompt=prompt : self.send_sample_prompt(prompt))
button_container.append(prompt_button)
else:
button = Gtk.Button(
label=_("Open Model Manager"),
tooltip_text=_("Open Model Manager"),
css_classes=["accent"]
)
button.connect('clicked', lambda *_ : self.manage_models_dialog.present(self))
button_container.append(button)
self.chat_welcome_screen = Adw.StatusPage(
icon_name="com.jeffser.Alpaca",
title="Alpaca",
description=_("Try one of these prompts") if len(self.local_models) > 0 else _("It looks like you don't have any models downloaded yet. Download models to get started!"),
child=button_container,
vexpand=True
)
self.chat_container.append(self.chat_welcome_screen)
def load_history(self):
logger.debug("Loading history")
@@ -1227,12 +1357,12 @@ Generate a title following these rules:
self.chats["order"] = []
for chat_name in self.chats["chats"].keys():
self.chats["order"].append(chat_name)
self.model_list_box.select_row(self.model_list_box.get_row_at_index(0))
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
last_model_used = self.convert_model_name(last_model_used, 0)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i)
for i, m in enumerate(self.local_models):
if m == last_model_used:
self.model_list_box.select_row(self.model_list_box.get_row_at_index(i))
break
except Exception as e:
logger.error(e)
@@ -1263,7 +1393,7 @@ Generate a title following these rules:
def clear_chat(self):
logger.info("Clearing chat")
for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.chats["selected_chat"]]["messages"] = []
self.chats["chats"][self.chats["selected_chat"]]["messages"] = {}
self.save_history()
def delete_chat(self, chat_name):
@@ -1308,10 +1438,11 @@ Generate a title following these rules:
def delete_model(self, model_name):
logger.debug("Deleting model")
response = connection_handler.simple_delete(f"{connection_handler.url}/api/delete", data={"name": model_name})
response = connection_handler.simple_delete(f"{connection_handler.URL}/api/delete", data={"name": model_name})
self.update_list_local_models()
if response.status_code == 200:
self.show_toast(_("Model deleted successfully"), self.manage_models_overlay)
self.change_model()
else:
self.manage_models_dialog.close()
self.connection_error()
@@ -1322,6 +1453,7 @@ Generate a title following these rules:
menu_model=self.chat_right_click_menu,
has_arrow=False,
halign=1,
height_request=125
)
self.selected_chat_row = chat_row
position = Gdk.Rectangle()
@@ -1372,17 +1504,17 @@ Generate a title following these rules:
def connect_remote(self, url, bearer_token):
logger.debug(f"Connecting to remote: {url}")
connection_handler.url = url
connection_handler.bearer_token = bearer_token
self.remote_url = connection_handler.url
connection_handler.URL = url
connection_handler.BEARER_TOKEN = bearer_token
self.remote_url = connection_handler.URL
self.remote_connection_entry.set_text(self.remote_url)
if self.verify_connection() == False: self.connection_error()
def connect_local(self):
logger.debug("Connecting to Alpaca's Ollama instance")
self.run_remote = False
connection_handler.bearer_token = None
connection_handler.url = f"http://127.0.0.1:{local_instance.port}"
connection_handler.BEARER_TOKEN = None
connection_handler.URL = f"http://127.0.0.1:{local_instance.port}"
local_instance.start()
if self.verify_connection() == False:
self.connection_error()
@@ -1392,7 +1524,7 @@ Generate a title following these rules:
def connection_error(self):
logger.error("Connection error")
if self.run_remote:
dialogs.reconnect_remote(self, connection_handler.url, connection_handler.bearer_token)
dialogs.reconnect_remote(self, connection_handler.URL, connection_handler.BEARER_TOKEN)
else:
local_instance.reset()
self.show_toast(_("There was an error with the local Ollama instance, so it has been reset"), self.main_overlay)
@@ -1403,15 +1535,15 @@ Generate a title following these rules:
if new_value != self.run_remote:
self.run_remote = new_value
if self.run_remote:
connection_handler.bearer_token = self.remote_bearer_token
connection_handler.url = self.remote_url
connection_handler.BEARER_TOKEN = self.remote_bearer_token
connection_handler.URL = self.remote_url
if self.verify_connection() == False:
self.connection_error()
else:
local_instance.stop()
else:
connection_handler.bearer_token = None
connection_handler.url = f"http://127.0.0.1:{local_instance.port}"
connection_handler.BEARER_TOKEN = None
connection_handler.URL = f"http://127.0.0.1:{local_instance.port}"
local_instance.start()
if self.verify_connection() == False:
self.connection_error()
@@ -1428,7 +1560,7 @@ Generate a title following these rules:
with tempfile.TemporaryDirectory() as temp_dir:
json_path = os.path.join(temp_dir, "data.json")
with open(json_path, "wb", encoding="utf-8") as json_file:
with open(json_path, "wb") as json_file:
json_file.write(json_data)
tar_path = os.path.join(temp_dir, chat_name)
@@ -1438,7 +1570,7 @@ Generate a title following these rules:
if os.path.exists(directory) and os.path.isdir(directory):
tar.add(directory, arcname=os.path.basename(directory))
with open(tar_path, "rb", encoding="utf-8") as tar:
with open(tar_path, "rb") as tar:
tar_content = tar.read()
file.replace_contents_async(
@@ -1466,7 +1598,7 @@ Generate a title following these rules:
with tempfile.TemporaryDirectory() as temp_dir:
tar_filename = os.path.join(temp_dir, "imported_chat.tar")
with open(tar_filename, "wb", encoding="utf-8") as tar_file:
with open(tar_filename, "wb") as tar_file:
tar_file.write(tar_content.get_data())
with tarfile.open(tar_filename, "r") as tar:
@@ -1576,7 +1708,7 @@ Generate a title following these rules:
chat_row = self.selected_chat_row
chat_name = chat_row.get_child().get_name()
action_name = action.get_name()
if action_name == 'delete_chat':
if action_name in ('delete_chat', 'delete_current_chat'):
dialogs.delete_chat(self, chat_name)
elif action_name in ('rename_chat', 'rename_current_chat'):
dialogs.rename_chat(self, chat_name, chat_row.get_child())
@@ -1625,7 +1757,8 @@ Generate a title following these rules:
self.attach_file(os.path.join(self.cache_dir, 'tmp/images/{}'.format(image_name)), 'image')
else:
self.show_toast(_("Image recognition is only available on specific models"), self.main_overlay)
except Exception as e: 'huh'
except Exception as e:
pass
def on_clipboard_paste(self, textview):
logger.debug("Pasting from clipboard")
@@ -1633,27 +1766,9 @@ Generate a title following these rules:
clipboard.read_text_async(None, self.cb_text_received)
clipboard.read_texture_async(None, self.cb_image_received)
def on_model_dropdown_setup(self, factory, list_item):
label = Gtk.Label()
label.set_ellipsize(2)
label.set_xalign(0)
list_item.set_child(label)
def on_model_dropdown_bind(self, factory, list_item):
label = list_item.get_child()
item = list_item.get_item()
label.set_text(item.get_string())
label.set_tooltip_text(item.get_string())
def setup_model_dropdown(self):
factory = Gtk.SignalListItemFactory()
factory.connect("setup", self.on_model_dropdown_setup)
factory.connect("bind", self.on_model_dropdown_bind)
self.model_drop_down.set_factory(factory)
def handle_enter_key(self):
self.send_message()
if not self.bot_message:
self.send_message()
return True
def __init__(self, **kwargs):
@@ -1664,7 +1779,7 @@ Generate a title following these rules:
if not os.path.exists(os.path.join(self.data_dir, "chats")):
os.makedirs(os.path.join(self.data_dir, "chats"))
key_controller = Gtk.EventControllerKey.new()
key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.handle_enter_key() if keyval==Gdk.KEY_Return else None)
key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.handle_enter_key() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
self.message_text_view.add_controller(key_controller)
self.set_help_overlay(self.shortcut_window)
self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash'])
@@ -1675,21 +1790,22 @@ Generate a title following these rules:
self.get_application().create_action('create_model_from_file', lambda *_: dialogs.create_model_from_file(self))
self.get_application().create_action('create_model_from_name', lambda *_: dialogs.create_model_from_name(self))
self.get_application().create_action('delete_chat', self.chat_actions)
self.get_application().create_action('delete_current_chat', self.current_chat_actions)
self.get_application().create_action('rename_chat', self.chat_actions)
self.get_application().create_action('rename_current_chat', self.current_chat_actions)
self.get_application().create_action('export_chat', self.chat_actions)
self.get_application().create_action('export_current_chat', self.current_chat_actions)
self.get_application().create_action('toggle_sidebar', lambda *_: self.split_view_overlay.set_show_sidebar(not self.split_view_overlay.get_show_sidebar()), ['F9'])
self.get_application().create_action('manage_models', lambda *_: self.manage_models_button_activate(), ['<primary>m'])
self.get_application().create_action('manage_models', lambda *_: self.manage_models_dialog.present(self), ['<primary>m'])
self.message_text_view.connect("paste-clipboard", self.on_clipboard_paste)
self.file_preview_remove_button.connect('clicked', lambda button : dialogs.remove_attached_file(self, button.get_name()))
self.add_chat_button.connect("clicked", lambda button : self.new_chat())
self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialogs.attach_file(self, file_filter))
self.create_model_name.get_delegate().connect("insert-text", self.check_alphanumeric)
self.create_model_name.get_delegate().connect("insert-text", lambda *_ : self.check_alphanumeric(*_, ['-', '.', '_']))
self.remote_connection_entry.connect("entry-activated", lambda entry : entry.set_css_classes([]))
self.remote_connection_switch.connect("notify", lambda pspec, user_data : self.connection_switched())
self.background_switch.connect("notify", lambda pspec, user_data : self.switch_run_on_background())
self.setup_model_dropdown()
self.set_focus(self.message_text_view)
if os.path.exists(os.path.join(self.config_dir, "server.json")):
with open(os.path.join(self.config_dir, "server.json"), "r", encoding="utf-8") as f:
data = json.load(f)
@@ -1719,17 +1835,17 @@ Generate a title following these rules:
self.remote_connection_entry.set_text(self.remote_url)
self.remote_bearer_token_entry.set_text(self.remote_bearer_token)
if self.run_remote:
connection_handler.bearer_token = self.remote_bearer_token
connection_handler.url = self.remote_url
connection_handler.BEARER_TOKEN = self.remote_bearer_token
connection_handler.URL = self.remote_url
self.remote_connection_switch.set_active(True)
else:
connection_handler.bearer_token = None
connection_handler.BEARER_TOKEN = None
self.remote_connection_switch.set_active(False)
connection_handler.url = f"http://127.0.0.1:{local_instance.port}"
connection_handler.URL = f"http://127.0.0.1:{local_instance.port}"
local_instance.start()
else:
local_instance.start()
connection_handler.url = f"http://127.0.0.1:{local_instance.port}"
connection_handler.URL = f"http://127.0.0.1:{local_instance.port}"
self.welcome_dialog.present(self)
if self.verify_connection() is False:
self.connection_error()

View File

@@ -12,13 +12,14 @@
<property name="title">Alpaca</property>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 800sp</condition>
<condition>max-width: 690sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter>
</object>
</child>
<property name="content">
<object class="AdwOverlaySplitView" id="split_view_overlay">
<property name="show-sidebar" bind-source="show_sidebar_button" bind-property="active" bind-flags="sync-create"/>
<property name="sidebar-width-fraction">0.4</property>
<property name="sidebar">
<object class="AdwToolbarView">
<child type="top">
@@ -75,30 +76,81 @@
<property name="orientation">0</property>
<property name="spacing">12</property>
<child>
<object class="GtkDropDown" id="model_drop_down">
<signal name="notify" handler="verify_if_image_can_be_used"/>
<property name="width-request">260</property>
<property name="enable-search">true</property>
<property name="tooltip-text">Select Model</property>
<property name="model">
<object class="GtkStringList" id="model_string_list">
<items>
</items>
<object class="GtkMenuButton" id="model_selector_button">
<property name="tooltip-text" translatable="yes">Select Model</property>
<property name="child">
<object class="GtkBox">
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Select a Model</property>
<property name="ellipsize">2</property>
</object>
</child>
<child>
<object class="GtkImage">
<property name="icon-name">down-symbolic</property>
</object>
</child>
</object>
</property>
<property name="halign">1</property>
<style>
<class name="raised"/>
</style>
<property name="popover">
<object class="GtkPopover" id="model_popover">
<property name="has-arrow">false</property>
<child>
<object class="GtkBox">
<property name="orientation">1</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton">
<property name="child">
<object class="GtkLabel">
<property name="label" translatable="yes">Manage Models</property>
<property name="justify">left</property>
<property name="halign">1</property>
</object>
</property>
<property name="hexpand">true</property>
<property name="tooltip-text" translatable="yes">Manage Models</property>
<property name="action-name">app.manage_models</property>
<signal name="clicked" handler="close_model_popup"/>
<style>
<class name="flat"/>
<class name="manage_models_button"/>
</style>
</object>
</child>
<child>
<object class="GtkSeparator"/>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="max-content-height">300</property>
<property name="propagate-natural-width">true</property>
<property name="propagate-natural-height">true</property>
<child>
<object class="GtkListBox" id="model_list_box">
<property name="hexpand">true</property>
<style>
<class name="navigation-sidebar"/>
<class name="model_list_box"/>
</style>
<signal name="row-selected" handler="change_model"/>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<!--<child>
<object class="GtkButton" id="manage_models_button">
<signal name="clicked" handler="manage_models_button_activate"/>
<property name="tooltip-text" translatable="yes">Manage Models</property>
<child>
<object class="AdwButtonContent">
<property name="icon-name">brain-augemnted-symbolic</property>
</object>
</child>
</object>
</child>-->
</object>
</property>
<child type="end">
@@ -215,6 +267,10 @@
<property name="top-margin">10</property>
<property name="bottom-margin">10</property>
<property name="hexpand">true</property>
<property name="input-hints">spellcheck</property>
<accessibility>
<property name="label" translatable="yes">Message text box</property>
</accessibility>
</object>
</child>
</object>
@@ -442,6 +498,9 @@
</object>
<object class="AdwDialog" id="manage_models_dialog">
<accessibility>
<property name="label" translatable="yes">Manage models dialog</property>
</accessibility>
<property name="can-close">true</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
@@ -476,12 +535,18 @@
</child>
<child type="top">
<object class="GtkSearchBar" id="model_searchbar">
<accessibility>
<property name="label" translatable="yes">Model search bar</property>
</accessibility>
<property name="key-capture-widget">AlpacaWindow</property>
<child>
<object class="GtkSearchEntry" id="searchentry">
<signal name="search-changed" handler="model_search_changed"/>
<property name="search-delay">100</property>
<property name="placeholder-text" translatable="yes">Search models</property>
<accessibility>
<property name="label" translatable="yes">Search models</property>
</accessibility>
</object>
</child>
</object>
@@ -517,7 +582,7 @@
</child>
<child>
<object class="GtkListBox" id="available_model_list_box">
<property name="selection-mode">single</property>
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
@@ -580,7 +645,7 @@
<child>
<object class="GtkListBox" id="model_tag_list_box">
<property name="valign">1</property>
<property name="selection-mode">single</property>
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
@@ -738,9 +803,12 @@
</object>
<object class="AdwDialog" id="file_preview_dialog">
<accessibility>
<property name="label" translatable="yes">File preview dialog</property>
</accessibility>
<property name="can-close">true</property>
<property name="width-request">450</property>
<property name="height-request">450</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToolbarView">
<child type="top">
@@ -805,8 +873,8 @@
<object class="AdwDialog" id="welcome_dialog">
<property name="can-close">false</property>
<property name="width-request">450</property>
<property name="height-request">450</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToolbarView">
<child type="bottom">
@@ -885,141 +953,6 @@
<property name="description" translatable="yes">Alpaca and its developers are not liable for any damages to devices or software resulting from the execution of code generated by an AI model. Please exercise caution and review the code carefully before running it.</property>
</object>
</child>
<child>
<object class="AdwStatusPage">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="title" translatable="yes">Featured Models</property>
<property name="description" translatable="yes">Alpaca works locally on your device, to start chatting you'll need an AI model, you can either pull models from this list or the 'Manage Models' menu later.
By downloading any model you accept their license agreement available on the model's website.
</property>
<child>
<object class="GtkListBox">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
<child>
<object class="AdwActionRow">
<property name="title" translatable="no">Llama3</property>
<property name="subtitle" translatable="yes">Built by Meta</property>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="vexpand">false</property>
<property name="icon-name">globe-symbolic</property>
<property name="valign">3</property>
<property name="name">https://ollama.com/library/llama3</property>
<property name="tooltip-text">https://ollama.com/library/llama3</property>
</object>
</child>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="pull_featured_model"/>
<property name="vexpand">false</property>
<property name="icon-name">folder-download-symbolic</property>
<property name="valign">3</property>
<property name="tooltip-text">Pull 'Llama3 (latest)'</property>
<style>
<class name="accent"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="no">Gemma2</property>
<property name="subtitle" translatable="yes">Built by Google DeepMind</property>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="vexpand">false</property>
<property name="icon-name">globe-symbolic</property>
<property name="valign">3</property>
<property name="name">https://ollama.com/library/gemma2</property>
<property name="tooltip-text">https://ollama.com/library/gemma2</property>
</object>
</child>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="pull_featured_model"/>
<property name="vexpand">false</property>
<property name="icon-name">folder-download-symbolic</property>
<property name="valign">3</property>
<property name="tooltip-text">Pull 'Gemma2 (latest)'</property>
<style>
<class name="accent"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="no">Phi3</property>
<property name="subtitle" translatable="yes">Built by Microsoft</property>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="vexpand">false</property>
<property name="icon-name">globe-symbolic</property>
<property name="valign">3</property>
<property name="name">https://ollama.com/library/phi3</property>
<property name="tooltip-text">https://ollama.com/library/phi3</property>
</object>
</child>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="pull_featured_model"/>
<property name="vexpand">false</property>
<property name="icon-name">folder-download-symbolic</property>
<property name="valign">3</property>
<property name="tooltip-text">Pull 'Phi3 (latest)'</property>
<style>
<class name="accent"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="title" translatable="no">Llava</property>
<property name="subtitle" translatable="yes">Multimodal AI with image recognition</property>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="vexpand">false</property>
<property name="icon-name">globe-symbolic</property>
<property name="valign">3</property>
<property name="name">https://ollama.com/library/llava</property>
<property name="tooltip-text">https://ollama.com/library/llava</property>
</object>
</child>
<child type="suffix">
<object class="GtkButton">
<signal name="clicked" handler="pull_featured_model"/>
<property name="vexpand">false</property>
<property name="icon-name">folder-download-symbolic</property>
<property name="valign">3</property>
<property name="tooltip-text">Pull 'Llava (latest)'</property>
<style>
<class name="accent"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
@@ -1054,25 +987,29 @@ By downloading any model you accept their license agreement available on the mod
</section>
</menu>
<menu id="secondary_menu">
<item>
<attribute name="label" translatable="yes">Rename Chat</attribute>
<attribute name="action">app.rename_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export Chat</attribute>
<attribute name="action">app.export_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Clear Chat</attribute>
<attribute name="action">app.clear</attribute>
</item>
</menu>
<menu id="chat_right_click_menu">
<section>
<item>
<attribute name="label" translatable="yes">Rename Chat</attribute>
<attribute name="action">app.rename_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export Chat</attribute>
<attribute name="action">app.export_current_chat</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Clear Chat</attribute>
<attribute name="action">app.clear</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Delete Chat</attribute>
<attribute name="action">app.delete_chat</attribute>
<attribute name="action">app.delete_current_chat</attribute>
</item>
</section>
</menu>
<menu id="chat_right_click_menu">
<section>
<item>
<attribute name="label" translatable="yes">Rename Chat</attribute>
<attribute name="action">app.rename_chat</attribute>
@@ -1082,6 +1019,12 @@ By downloading any model you accept their license agreement available on the mod
<attribute name="action">app.export_chat</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Delete Chat</attribute>
<attribute name="action">app.delete_chat</attribute>
</item>
</section>
</menu>
<menu id="create_model_menu">
<section>

View File

@@ -15,4 +15,6 @@ msgmerge --no-fuzzy-matching -U po/nb_NO.po po/alpaca.pot
echo "Updating Bengali"
msgmerge --no-fuzzy-matching -U po/bn.po po/alpaca.pot
echo "Updating Simplified Chinese"
msgmerge --no-fuzzy-matching -U po/zh_CN.po po/alpaca.pot
msgmerge --no-fuzzy-matching -U po/zh_CN.po po/alpaca.pot
echo "Updating Hindi"
msgmerge --no-fuzzy-matching -U po/hi.po po/alpaca.pot