92 Commits
1.0.4 ... 1.0.6

Author SHA1 Message Date
jeffser
fa22647acd Output Ollama version when it is launched 2024-08-04 23:29:32 -06:00
jeffser
dd5d82fe7a Updated spanish + prepared the other languages 2024-08-04 23:17:00 -06:00
jeffser
98b179aeb5 Preparing for 1.0.6 2024-08-04 22:57:52 -06:00
jeffser
e1f1c005a0 Changed model dropdown width and moved manage models button to primary menu 2024-08-04 22:49:43 -06:00
jeffser
6e226c5a4f Fixed selected model changes when entering manage models dialog 2024-08-04 22:42:49 -06:00
jeffser
7440fa5a37 Fixed GGUF support and enter key handling 2024-08-04 22:23:02 -06:00
jeffser
4fe204605a Linting code 2024-08-04 22:09:37 -06:00
jeffser
4446b42b82 Linting code 2024-08-04 21:57:57 -06:00
jeffser
4b6cd17d0a Linting code 2024-08-04 21:43:23 -06:00
jeffser
1a6e74271c Linting code 2024-08-04 21:27:12 -06:00
jeffser
6ba3719031 Linting code 2024-08-04 21:20:47 -06:00
jeffser
dd95e3df7e Linting code 2024-08-04 21:11:00 -06:00
jeffser
69fd7853c8 Added explanations 2024-08-04 21:03:26 -06:00
jeffser
c01c478ffe Fixed 2024-08-04 20:59:47 -06:00
jeffser
f8be1da83a Fixed 2024-08-04 20:54:59 -06:00
jeffser
3a7625486e Disable 'line is too long' message 2024-08-04 20:53:18 -06:00
jeffser
fdc3b6c573 Maybe this time 2024-08-04 20:51:56 -06:00
jeffser
76939ed51f Fixed 2024-08-04 20:49:44 -06:00
jeffser
b9cf761f4a Fixed 2024-08-04 20:47:54 -06:00
jeffser
4c515ba541 bruh 2024-08-04 20:44:47 -06:00
jeffser
d7c3595bf1 added rcfile 2024-08-04 20:43:11 -06:00
jeffser
1fbd6a0824 Lint fixes and added lint config file 2024-08-04 20:41:35 -06:00
jeffser
ccb59c7f02 Fix 2024-08-04 20:34:08 -06:00
jeffser
04bef3e82a Skip available_models_description.py 2024-08-04 20:33:39 -06:00
jeffser
17105b98ed Fix for lint 2024-08-04 20:31:40 -06:00
jeffser
4bff1515a9 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-04 20:30:30 -06:00
jeffser
0a75893346 Fix 2024-08-04 20:30:24 -06:00
Jeffry Samuel
2ed92467f9 Update pylint.yml 2024-08-04 20:28:47 -06:00
jeffser
634ac122d9 Fixed file so it passes the linter 2024-08-04 20:28:27 -06:00
Jeffry Samuel
44640b7e53 Create pylint.yml 2024-08-04 20:20:23 -06:00
jeffser
47e7b22a7e Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-04 20:01:41 -06:00
jeffser
918928d4bb Fixed image tooltips 2024-08-04 20:01:34 -06:00
Jeffry Samuel
69fc172779 Update README.md 2024-08-04 19:54:25 -06:00
Jeffry Samuel
d84dabbe4d Update feature_request.md 2024-08-04 19:52:22 -06:00
Jeffry Samuel
23114210c4 Update bug_report.md 2024-08-04 19:52:12 -06:00
jeffser
ea80e5a223 Remade doap 2024-08-04 19:50:06 -06:00
jeffser
6087f31d41 Removed whitespace 2024-08-04 19:44:10 -06:00
jeffser
30ee292a32 Updated doap 2024-08-04 19:43:22 -06:00
Jeffry Samuel
705a9319f5 Update Alpaca.doap 2024-08-04 19:37:34 -06:00
jeffser
c789d9d87c Added doap 2024-08-04 19:36:41 -06:00
jeffser
a7681b5505 Removed that workflow 2024-08-04 19:21:58 -06:00
Jeffry Samuel
9e74d8af0b Create meson-build.yml 2024-08-04 19:21:04 -06:00
jeffser
b52061f849 Removed that workflow 2024-08-04 19:17:46 -06:00
Jeffry Samuel
01b875c283 Update flatpak-lint.yml 2024-08-04 19:15:06 -06:00
Jeffry Samuel
4cc3b78321 Update flatpak-lint.yml 2024-08-04 19:11:06 -06:00
Jeffry Samuel
6205db87e6 Update flatpak-lint.yml 2024-08-04 19:07:42 -06:00
Jeffry Samuel
518633b153 Update flatpak-lint.yml 2024-08-04 19:05:23 -06:00
Jeffry Samuel
988ee7b7e7 Update flatpak-lint.yml 2024-08-04 19:03:44 -06:00
Jeffry Samuel
cdadde60ce Update flatpak-lint.yml 2024-08-04 19:00:00 -06:00
Jeffry Samuel
4bb01d86d9 Update flatpak-lint.yml 2024-08-04 18:55:34 -06:00
Jeffry Samuel
4cac43520f Testing 2024-08-04 18:53:36 -06:00
Jeffry Samuel
d6dddd16f1 Update flatpak-builder.yml 2024-08-04 18:44:24 -06:00
Jeffry Samuel
c0da054635 Update flatpak-builder.yml 2024-08-04 18:43:13 -06:00
jeffser
2b4d94ca55 fixed logger in dialogs.py 2024-08-04 18:35:26 -06:00
Jeffry Samuel
e8e564738a Update com.jeffser.Alpaca.json 2024-08-04 18:26:06 -06:00
Jeffry Samuel
d48fbd8b62 Update and rename flatpak-build.yml to flatpak-builder.yml 2024-08-04 18:22:50 -06:00
Jeffry Samuel
c1f80f209e Update flatpak-build.yml 2024-08-04 18:18:44 -06:00
Jeffry Samuel
ed6b32c827 Update flatpak-build.yml 2024-08-04 18:16:50 -06:00
Jeffry Samuel
fc436fd352 Update flatpak-build.yml 2024-08-04 18:14:45 -06:00
Jeffry Samuel
ee6fdb1ca1 Update flatpak-build.yml 2024-08-04 18:12:37 -06:00
Jeffry Samuel
988db30355 Update flatpak-build.yml 2024-08-04 18:12:19 -06:00
Jeffry Samuel
ea98ee5e99 Update flatpak-build.yml 2024-08-04 18:10:29 -06:00
Jeffry Samuel
b8d1d43822 Testing 2024-08-04 18:07:27 -06:00
jeffser
0d017c6d14 Changed shortcuts to standards 2024-08-04 17:50:52 -06:00
jeffser
2825e9a003 Fixed window and elements dimensions 2024-08-04 17:41:19 -06:00
jeffser
6e9ddfcbf2 Added sponsor link to about dialog 2024-08-04 17:11:06 -06:00
jeffser
378689be39 Removed support dialog 2024-08-04 16:50:39 -06:00
jeffser
31858fad12 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-04 16:26:52 -06:00
jeffser
60351d629d Bettter handling of enter key on message entry 2024-08-04 16:26:46 -06:00
aritra saha
715a97159a small update (#190)
* Update bn.po

* Update bn.po

* Create hi.po

* Rename hi.po to po/hi.po

* Update hi.po

* Update hi.po
2024-08-04 14:24:58 -06:00
jeffser
b48ce28b35 Crediting every translator 2024-08-03 15:13:22 -06:00
Jeffry Samuel
7ab0448cd3 Better README !!! 2024-08-03 15:06:56 -06:00
Jeffry Samuel
5f6642fa63 Update README.md 2024-08-03 14:12:42 -06:00
Aleksana
5a0d1ed408 Better handling of standard paths (#187) 2024-08-03 14:07:14 -06:00
Aleksana
131e8fb6be Updated translations (#188) 2024-08-03 14:02:47 -06:00
jeffser
1c7fb8ef93 Updated translations 2024-08-03 00:43:10 -06:00
jeffser
8c0ec3957f Preparing for 1.0.5 2024-08-02 23:56:17 -06:00
jeffser
72063a15d9 Better check for message finishing 2024-08-02 23:48:03 -06:00
jeffser
0d1b15aafc New feature: Regenerate response 2024-08-02 23:42:35 -06:00
jeffser
ca10369bdc Added message to support dialog 2024-08-02 21:44:19 -06:00
jeffser
42af75d8d2 typo 2024-08-02 20:53:19 -06:00
jeffser
a02871dd28 'S fixed again :3 2024-08-02 20:50:04 -06:00
jeffser
e65a8bc648 Proper GGUF / name Model pulling 2024-08-02 20:47:04 -06:00
jeffser
b373b6a34f Sidebar button 2024-08-02 16:44:24 -06:00
jeffser
6d6a0255e2 Better model name handling internally 2024-08-02 16:00:47 -06:00
jeffser
003d6a3d5f Restore last model used 2024-08-02 15:30:03 -06:00
jeffser
77a2c60fe5 Fixed message entry shadow 2024-08-02 15:19:28 -06:00
jeffser
ac3bd699ee Changed width request for model dropdown 2024-08-02 14:51:08 -06:00
jeffser
596498c81e Fixed 'S problem with title generation 2024-08-02 14:42:11 -06:00
jeffser
c95f764c77 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-02 14:39:26 -06:00
jeffser
5c5be05843 Reverted back to standard styles 2024-08-02 14:39:19 -06:00
aritra saha
3fb26ec49e updated translation (#180)
* Update bn.po

* Update bn.po

* Update bn.po

* Update bn.po

* Update zh_CN.po
2024-08-02 11:19:23 -06:00
32 changed files with 7604 additions and 4001 deletions

View File

@@ -6,7 +6,7 @@ labels: bug
assignees: '' assignees: ''
--- ---
<!--Please be aware that GNOME Code of Conduct applies to Alpaca, https://conduct.gnome.org/-->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@@ -6,7 +6,7 @@ labels: enhancement
assignees: '' assignees: ''
--- ---
<!--Please be aware that GNOME Code of Conduct applies to Alpaca, https://conduct.gnome.org/-->
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

18
.github/workflows/flatpak-builder.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# .github/workflows/flatpak-build.yml
on:
workflow_dispatch:
name: Flatpak Build
jobs:
flatpak:
name: "Flatpak"
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-46
options: --privileged
steps:
- uses: actions/checkout@v4
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: Alpaca.flatpak
manifest-path: com.jeffser.Alpaca.json
cache-key: flatpak-builder-${{ github.sha }}

24
.github/workflows/pylint.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Pylint
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
- name: Analysing the code with pylint
run: |
pylint --rcfile=.pylintrc $(git ls-files '*.py' | grep -v 'src/available_models_descriptions.py')

14
.pylintrc Normal file
View File

@@ -0,0 +1,14 @@
[MASTER]
[MESSAGES CONTROL]
disable=undefined-variable, line-too-long, missing-function-docstring, consider-using-f-string, import-error
[FORMAT]
max-line-length=200
# Reasons for removing some checks:
# undefined-variable: _() is used by the translator on build time but it is not defined on the scripts
# line-too-long: I... I'm too lazy to make the lines shorter, maybe later
# missing-function-docstring I'm not adding a docstring to all the functions, most are self explanatory
# consider-using-f-string I can't use f-string because of the translator
# import-error The linter doesn't have access to all the libraries that the project itself does

34
Alpaca.doap Normal file
View File

@@ -0,0 +1,34 @@
<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
<name xml:lang="en">Alpaca</name>
<shortdesc xml:lang="en">An Ollama client made with GTK4 and Adwaita</shortdesc>
<homepage rdf:resource="https://jeffser.com/alpaca" />
<bug-database rdf:resource="https://github.com/Jeffser/Alpaca/issues"/>
<programming-language>Python</programming-language>
<platform>GTK 4</platform>
<platform>Libadwaita</platform>
<maintainer>
<foaf:Person>
<foaf:name>Jeffry Samuel</foaf:name>
<foaf:mbox rdf:resource="mailto:jeffrysamuer@gmail.com"/>
<foaf:account>
<foaf:OnlineAccount>
<foaf:accountServiceHomepage rdf:resource="https://github.com"/>
<foaf:accountName>jeffser</foaf:accountName>
</foaf:OnlineAccount>
</foaf:account>
<foaf:account>
<foaf:OnlineAccount>
<foaf:accountServiceHomepage rdf:resource="https://gitlab.gnome.org"/>
<foaf:accountName>jeffser</foaf:accountName>
</foaf:OnlineAccount>
</foaf:account>
</foaf:Person>
</maintainer>
</Project>

View File

@@ -11,7 +11,11 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
> [!WARNING] > [!WARNING]
> This project is not affiliated at all with Ollama, I'm not responsible for any damages to your device or software caused by running code given by any AI models. > This project is not affiliated at all with Ollama, I'm not responsible for any damages to your device or software caused by running code given by any AI models.
> [!IMPORTANT]
> Please be aware that [GNOME Code of Conduct](https://conduct.gnome.org) applies to Alpaca before interacting with this repository.
## Features! ## Features!
- Talk to multiple models in the same conversation - Talk to multiple models in the same conversation
- Pull and delete models from the app - Pull and delete models from the app
- Image recognition - Image recognition
@@ -21,47 +25,37 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
- Notifications - Notifications
- Import / Export chats - Import / Export chats
- Delete / Edit messages - Delete / Edit messages
- Regenerate messages
- YouTube recognition (Ask questions about a YouTube video using the transcript) - YouTube recognition (Ask questions about a YouTube video using the transcript)
- Website recognition (Ask questions about a certain website by parsing the url) - Website recognition (Ask questions about a certain website by parsing the url)
## Screenies ## Screenies
Chatting with a model | Image recognition | Code highlighting
:--------------------:|:-----------------:|:----------------------:
![Screenshot from 2024-05-12 19-58-28](https://jeffser.com/images/alpaca/screenie1.png) | ![Screenshot from 2024-05-12 20-01-08](https://jeffser.com/images/alpaca/screenie2.png) | ![Screenshot from 2024-05-12 20-01-31](https://jeffser.com/images/alpaca/screenie3.png)
## Preview Normal conversation | Image recognition | Code highlighting | YouTube transcription | Model management
1. Clone repo using Gnome Builder :------------------:|:-----------------:|:-----------------:|:---------------------:|:----------------:
2. Press the `run` button ![screenie1](https://jeffser.com/images/alpaca/screenie1.png) | ![screenie2](https://jeffser.com/images/alpaca/screenie2.png) | ![screenie3](https://jeffser.com/images/alpaca/screenie3.png) | ![screenie4](https://jeffser.com/images/alpaca/screenie4.png) | ![screenie5](https://jeffser.com/images/alpaca/screenie5.png)
## Instalation ## Translators
1. Go to the `releases` page
2. Download the latest flatpak package
3. Open it
## Ollama session tips Language | Contributors
:----------------------|:-----------
### Change the port of the integrated Ollama instance 🇷🇺 Russian | [Alex K](https://github.com/alexkdeveloper)
Go to `~/.var/app/com.jeffser.Alpaca/config/server.json` and change the `"local_port"` value, by default it is `11435`. 🇪🇸 Spanish | [Jeffry Samuel](https://github.com/jeffser)
🇫🇷 French | [Louis Chauvet-Villaret](https://github.com/loulou64490) , [Théo FORTIN](https://github.com/topiga)
### Backup all the chats 🇧🇷 Brazilian Portuguese | [Daimar Stein](https://github.com/not-a-dev-stein)
The chat data is located in `~/.var/app/com.jeffser.Alpaca/data/chats` you can copy that directory wherever you want to. 🇳🇴 Norwegian | [CounterFlow64](https://github.com/CounterFlow64)
🇮🇳 Bengali | [Aritra Saha](https://github.com/olumolu)
### Force showing the welcome dialog 🇨🇳 Simplified Chinese | [Yuehao Sui](https://github.com/8ar10der) , [Aleksana](https://github.com/Aleksanaa)
To do that you just need to delete the file `~/.var/app/com.jeffser.Alpaca/config/server.json`, this won't affect your saved chats or models.
### Add/Change environment variables for Ollama
You can change anything except `$HOME` and `$OLLAMA_HOST`, to do this go to `~/.var/app/com.jeffser.Alpaca/config/server.json` and change `ollama_overrides` accordingly, some overrides are available to change on the GUI.
--- ---
## Thanks ## Thanks
- [not-a-dev-stein](https://github.com/not-a-dev-stein) for their help with requesting a new icon, bug reports and the translation to Brazilian Portuguese
- [not-a-dev-stein](https://github.com/not-a-dev-stein) for their help with requesting a new icon and bug reports
- [TylerLaBree](https://github.com/TylerLaBree) for their requests and ideas - [TylerLaBree](https://github.com/TylerLaBree) for their requests and ideas
- [Alexkdeveloper](https://github.com/alexkdeveloper) for their help translating the app to Russian
- [Imbev](https://github.com/imbev) for their reports and suggestions - [Imbev](https://github.com/imbev) for their reports and suggestions
- [Nokse](https://github.com/Nokse22) for their contributions to the UI and table rendering - [Nokse](https://github.com/Nokse22) for their contributions to the UI and table rendering
- [Louis Chauvet-Villaret](https://github.com/loulou64490) for their suggestions and help translating the app to French - [Louis Chauvet-Villaret](https://github.com/loulou64490) for their suggestions
- [CounterFlow64](https://github.com/CounterFlow64) for their help translating the app to Norwegian - [Aleksana](https://github.com/Aleksanaa) for her help with better handling of directories
- Sponsors for giving me enough money to be able to take a ride to my campus every time I need to <3
## About forks - Everyone that has shared kind words of encouragement!
If you want to fork this... I mean, I think it would be better if you start from scratch, my code isn't well documented at all, but if you really want to, please give me some credit, that's all I ask for... And maybe a donation (joke)

View File

@@ -122,16 +122,16 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"url": "https://github.com/ollama/ollama/releases/download/v0.3.0/ollama-linux-amd64", "url": "https://github.com/ollama/ollama/releases/download/v0.3.3/ollama-linux-amd64",
"sha256": "b8817c34882c7ac138565836ac1995a2c61261a79315a13a0aebbfe5435da855", "sha256": "2b2a4ee4c86fa5b09503e95616bd1b3ee95238b1b3bf12488b9c27c66b84061a",
"only-arches": [ "only-arches": [
"x86_64" "x86_64"
] ]
}, },
{ {
"type": "file", "type": "file",
"url": "https://github.com/ollama/ollama/releases/download/v0.3.0/ollama-linux-arm64", "url": "https://github.com/ollama/ollama/releases/download/v0.3.3/ollama-linux-arm64",
"sha256": "64be908749212052146f1008dd3867359c776ac1766e8d86291886f53d294d4d", "sha256": "28fddbea0c161bc539fd08a3dc78d51413cfe8da97386cb39420f4f30667e22c",
"only-arches": [ "only-arches": [
"aarch64" "aarch64"
] ]
@@ -145,7 +145,7 @@
"sources" : [ "sources" : [
{ {
"type" : "git", "type" : "git",
"url" : "file:///home/tentri/Documents/Alpaca", "url": "https://github.com/Jeffser/Alpaca.git",
"branch" : "main" "branch" : "main"
} }
] ]

View File

@@ -80,6 +80,49 @@
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url> <url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url> <url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
<releases> <releases>
<release version="1.0.6" date="2024-08-04">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.6</url>
<description>
<p>New</p>
<ul>
<li>Changed shortcuts to standards</li>
<li>Moved 'Manage Models' button to primary menu</li>
<li>Stable support for GGUF model files</li>
<li>General optimizations</li>
</ul>
<p>Fixes</p>
<ul>
<li>Better handling of enter key (important for Japanese input)</li>
<li>Removed sponsor dialog</li>
<li>Added sponsor link in about dialog</li>
<li>Changed window and elements dimensions</li>
<li>Selected model changes when entering model manager</li>
<li>Better image tooltips</li>
<li>GGUF Support</li>
</ul>
</description>
</release>
<release version="1.0.5" date="2024-08-02">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.5</url>
<description>
<p>New</p>
<ul>
<li>Regenerate any response, even if they are incomplete</li>
<li>Support for pulling models by name:tag</li>
<li>Stable support for GGUF model files</li>
<li>Restored sidebar toggle button</li>
</ul>
<p>Fixes</p>
<ul>
<li>Reverted back to standard styles</li>
<li>Fixed generated titles having "'S" for some reason</li>
<li>Changed min width for model dropdown</li>
<li>Changed message entry shadow</li>
<li>The last model used is now restored when the user changes chat</li>
<li>Better check for message finishing</li>
</ul>
</description>
</release>
<release version="1.0.4" date="2024-08-01"> <release version="1.0.4" date="2024-08-01">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.4</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.4</url>
<description> <description>

View File

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

File diff suppressed because it is too large Load Diff

1163
po/bn.po

File diff suppressed because it is too large Load Diff

1159
po/es.po

File diff suppressed because it is too large Load Diff

948
po/fr.po

File diff suppressed because it is too large Load Diff

2169
po/hi.po Normal file

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

976
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

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

View File

@@ -1,15 +1,19 @@
# connection_handler.py # connection_handler.py
import json, requests """
Handles requests to remote and integrated instances of Ollama
"""
import json
import requests
#OK=200 response.status_code #OK=200 response.status_code
url = None URL = None
bearer_token = None BEARER_TOKEN = None
def get_headers(include_json:bool) -> dict: def get_headers(include_json:bool) -> dict:
headers = {} headers = {}
if include_json: if include_json:
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
if bearer_token: if BEARER_TOKEN:
headers["Authorization"] = "Bearer {}".format(bearer_token) headers["Authorization"] = "Bearer {}".format(BEARER_TOKEN)
return headers if len(headers.keys()) > 0 else None return headers if len(headers.keys()) > 0 else None
def simple_get(connection_url:str) -> dict: def simple_get(connection_url:str) -> dict:

View File

@@ -1,11 +1,15 @@
# dialogs.py # dialogs.py
"""
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf Handles UI dialogs
"""
import os import os
import logging
from pytube import YouTube from pytube import YouTube
from html2text import html2text from html2text import html2text
from gi.repository import Adw, Gtk
from . import connection_handler from . import connection_handler
logger = logging.getLogger(__name__)
# CLEAR CHAT | WORKS # CLEAR CHAT | WORKS
def clear_chat_response(self, dialog, task): def clear_chat_response(self, dialog, task):
@@ -54,9 +58,11 @@ def delete_chat(self, chat_name):
# RENAME CHAT | WORKS # RENAME CHAT | WORKS
def rename_chat_response(self, dialog, task, old_chat_name, entry, label_element): def rename_chat_response(self, dialog, task, old_chat_name, entry, label_element):
if not entry: return if not entry:
return
new_chat_name = entry.get_text() new_chat_name = entry.get_text()
if old_chat_name == new_chat_name: return if old_chat_name == new_chat_name:
return
if new_chat_name and (task is None or dialog.choose_finish(task) == "rename"): if new_chat_name and (task is None or dialog.choose_finish(task) == "rename"):
self.rename_chat(old_chat_name, new_chat_name, label_element) self.rename_chat(old_chat_name, new_chat_name, label_element)
@@ -82,7 +88,8 @@ def rename_chat(self, chat_name, label_element):
def new_chat_response(self, dialog, task, entry): def new_chat_response(self, dialog, task, entry):
chat_name = _("New Chat") chat_name = _("New Chat")
if entry is not None and entry.get_text() != "": chat_name = entry.get_text() if entry is not None and entry.get_text() != "":
chat_name = entry.get_text()
if chat_name and (task is None or dialog.choose_finish(task) == "create"): if chat_name and (task is None or dialog.choose_finish(task) == "create"):
self.new_chat(chat_name) self.new_chat(chat_name)
@@ -224,7 +231,7 @@ def create_model_from_existing_response(self, dialog, task, dropdown):
def create_model_from_existing(self): def create_model_from_existing(self):
string_list = Gtk.StringList() string_list = Gtk.StringList()
for model in self.local_models: for model in self.local_models:
string_list.append(model) string_list.append(self.convert_model_name(model, 0))
dropdown = Gtk.DropDown() dropdown = Gtk.DropDown()
dropdown.set_model(string_list) dropdown.set_model(string_list)
@@ -243,20 +250,41 @@ def create_model_from_existing(self):
) )
def create_model_from_file_response(self, file_dialog, result): def create_model_from_file_response(self, file_dialog, result):
try: file = file_dialog.open_finish(result) try:
except: file = file_dialog.open_finish(result)
self.logger.error(e)
return
try: try:
self.create_model(file.get_path(), True) self.create_model(file.get_path(), True)
except Exception as e: except Exception as e:
self.logger.error(e) logger.error(e)
self.show_toast(_("An error occurred while creating the model"), self.main_overlay) self.show_toast(_("An error occurred while creating the model"), self.main_overlay)
except Exception as e:
logger.error(e)
def create_model_from_file(self): def create_model_from_file(self):
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_gguf) file_dialog = Gtk.FileDialog(default_filter=self.file_filter_gguf)
file_dialog.open(self, None, lambda file_dialog, result: create_model_from_file_response(self, file_dialog, result)) file_dialog.open(self, None, lambda file_dialog, result: create_model_from_file_response(self, file_dialog, result))
def create_model_from_name_response(self, dialog, task, entry):
model = entry.get_text().lower().strip()
if dialog.choose_finish(task) == 'accept' and model:
self.pull_model(model)
def create_model_from_name(self):
entry = Gtk.Entry()
entry.get_delegate().connect("insert-text", self.check_alphanumeric)
dialog = Adw.AlertDialog(
heading=_("Pull Model"),
body=_("Input the name of the model in this format\nname:tag"),
extra_child=entry
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, entry=entry: create_model_from_name_response(self, dialog, task, entry)
)
# FILE CHOOSER | WORKS # FILE CHOOSER | WORKS
def attach_file_response(self, file_dialog, result): def attach_file_response(self, file_dialog, result):
@@ -265,24 +293,24 @@ def attach_file_response(self, file_dialog, result):
"image": ["png", "jpeg", "jpg", "webp", "gif"], "image": ["png", "jpeg", "jpg", "webp", "gif"],
"pdf": ["pdf"] "pdf": ["pdf"]
} }
try: file = file_dialog.open_finish(result) try:
except: file = file_dialog.open_finish(result)
self.logger.error(e) except Exception as e:
logger.error(e)
return return
extension = file.get_path().split(".")[-1] extension = file.get_path().split(".")[-1]
file_type = next(key for key, value in file_types.items() if extension in value) file_type = next(key for key, value in file_types.items() if extension in value)
if not file_type: return if not file_type:
return
if file_type == 'image' and not self.verify_if_image_can_be_used(): if file_type == 'image' and not self.verify_if_image_can_be_used():
self.show_toast(_("Image recognition is only available on specific models"), self.main_overlay) self.show_toast(_("Image recognition is only available on specific models"), self.main_overlay)
return return
self.attach_file(file.get_path(), file_type) self.attach_file(file.get_path(), file_type)
def attach_file(self, file_filter):
def attach_file(self, filter): file_dialog = Gtk.FileDialog(default_filter=file_filter)
file_dialog = Gtk.FileDialog(default_filter=filter)
file_dialog.open(self, None, lambda file_dialog, result: attach_file_response(self, file_dialog, result)) file_dialog.open(self, None, lambda file_dialog, result: attach_file_response(self, file_dialog, result))
# YouTube caption | WORKS # YouTube caption | WORKS
def youtube_caption_response(self, dialog, task, video_url, caption_drop_down): def youtube_caption_response(self, dialog, task, video_url, caption_drop_down):
@@ -300,7 +328,7 @@ def youtube_caption_response(self, dialog, task, video_url, caption_drop_down):
if not os.path.exists(os.path.join(self.cache_dir, 'tmp/youtube')): if not os.path.exists(os.path.join(self.cache_dir, 'tmp/youtube')):
os.makedirs(os.path.join(self.cache_dir, 'tmp/youtube')) os.makedirs(os.path.join(self.cache_dir, 'tmp/youtube'))
file_path = os.path.join(os.path.join(self.cache_dir, 'tmp/youtube'), f'{yt.title} ({selected_caption.split(" | ")[0]})') file_path = os.path.join(os.path.join(self.cache_dir, 'tmp/youtube'), f'{yt.title} ({selected_caption.split(" | ")[0]})')
with open(file_path, 'w+') as f: with open(file_path, 'w+', encoding="utf-8") as f:
f.write(text) f.write(text)
self.attach_file(file_path, 'youtube') self.attach_file(file_path, 'youtube')
@@ -312,7 +340,8 @@ def youtube_caption(self, video_url):
self.show_toast(_("This video does not have any transcriptions"), self.main_overlay) self.show_toast(_("This video does not have any transcriptions"), self.main_overlay)
return return
caption_list = Gtk.StringList() caption_list = Gtk.StringList()
for caption in captions: caption_list.append("{} | {}".format(caption.name, caption.code)) for caption in captions:
caption_list.append("{} | {}".format(caption.name, caption.code))
caption_drop_down = Gtk.DropDown( caption_drop_down = Gtk.DropDown(
enable_search=True, enable_search=True,
model=caption_list model=caption_list
@@ -348,7 +377,7 @@ def attach_website_response(self, dialog, task, url):
os.makedirs('/tmp/alpaca/websites/') os.makedirs('/tmp/alpaca/websites/')
md_name = self.generate_numbered_name('website.md', os.listdir('/tmp/alpaca/websites')) md_name = self.generate_numbered_name('website.md', os.listdir('/tmp/alpaca/websites'))
file_path = os.path.join('/tmp/alpaca/websites/', md_name) file_path = os.path.join('/tmp/alpaca/websites/', md_name)
with open(file_path, 'w+') as f: with open(file_path, 'w+', encoding="utf-8") as f:
f.write('{}\n\n{}'.format(url, md)) f.write('{}\n\n{}'.format(url, md))
self.attach_file(file_path, 'website') self.attach_file(file_path, 'website')
else: else:
@@ -369,31 +398,3 @@ def attach_website(self, url):
cancellable = None, cancellable = None,
callback = lambda dialog, task, url=url: attach_website_response(self, dialog, task, url) callback = lambda dialog, task, url=url: attach_website_response(self, dialog, task, url)
) )
# Begging for money :3
def support_response(self, dialog, task):
res = dialog.choose_finish(task)
if res == 'later': return
elif res == 'support':
self.show_toast(_("Thank you!"), self.main_overlay)
os.system('xdg-open https://github.com/sponsors/Jeffser')
self.show_support = False
self.save_server_config()
def support(self):
dialog = Adw.AlertDialog(
heading=_("Support"),
body=_("Are you enjoying Alpaca? Consider sponsoring the project!"),
close_response="nope"
)
dialog.add_response("nope", _("Don't show again"))
dialog.set_response_appearance("nope", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.add_response("later", _("Later"))
dialog.add_response("support", _("Support"))
dialog.set_response_appearance("support", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task: support_response(self, dialog, task)
)

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"><g fill="#222222"><path d="m 7.957031 2 c -0.082031 0 -0.164062 0.003906 -0.246093 0.007812 c -0.1875 0.011719 -0.375 0.03125 -0.5625 0.0625 c -1.582032 0.226563 -3.007813 1.070313 -3.96875 2.34375 c -0.804688 1.074219 -1.183594 2.332032 -1.179688 3.585938 h 2.003906 c 0 -0.832031 0.253906 -1.671875 0.796875 -2.398438 c 1.335938 -1.777343 3.820313 -2.113281 5.597657 -0.78125 c 0.429687 0.320313 0.769531 0.734376 1.03125 1.1875 h -1.4375 c -0.550782 0 -1 0.449219 -1 1 v 1 h 6 v -6 h -1 c -0.550782 0 -1 0.449219 -1 1 v 1.6875 c -1.113282 -1.695312 -3.007813 -2.710937 -5.039063 -2.695312 z m 0 0"/><path d="m 8.035156 15.007812 c 0.082032 0 0.164063 -0.003906 0.246094 -0.007812 c 0.1875 -0.011719 0.375 -0.03125 0.5625 -0.0625 c 1.582031 -0.226562 3.007812 -1.066406 3.96875 -2.34375 c 0.804688 -1.074219 1.183594 -2.332031 1.179688 -3.585938 h -2.003907 c -0.003906 0.832032 -0.257812 1.675782 -0.796875 2.398438 c -1.335937 1.777344 -3.820312 2.113281 -5.597656 0.78125 c -0.429688 -0.320312 -0.769531 -0.734375 -1.03125 -1.1875 h 1.4375 c 0.550781 0 1 -0.449219 1 -1 v -1 h -6 v 6 h 1 c 0.550781 0 1 -0.449219 1 -1 v -1.6875 c 1.113281 1.695312 3.007812 2.710938 5.035156 2.695312 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

25
src/internal.py Normal file
View File

@@ -0,0 +1,25 @@
# internal.py
"""
Handles paths, they can be different if the app is running as a Flatpak
"""
import os
APP_ID = "com.jeffser.Alpaca"
IN_FLATPAK = bool(os.getenv("FLATPAK_ID"))
def get_xdg_home(env, default):
if IN_FLATPAK:
return os.getenv(env)
base = os.getenv(env) or os.path.expanduser(default)
path = os.path.join(base, APP_ID)
if not os.path.exists(path):
os.makedirs(path)
return path
data_dir = get_xdg_home("XDG_DATA_HOME", "~/.local/share")
config_dir = get_xdg_home("XDG_CONFIG_HOME", "~/.config")
cache_dir = get_xdg_home("XDG_CACHE_HOME", "~/.cache")
source_dir = os.path.abspath(os.path.dirname(__file__))

View File

@@ -1,29 +1,35 @@
# local_instance.py # local_instance.py
import subprocess, os, threading """
Handles running, stopping and resetting the integrated Ollama instance
"""
import subprocess
import os
from time import sleep from time import sleep
from logging import getLogger from logging import getLogger
from .internal import data_dir, cache_dir
logger = getLogger(__name__) logger = getLogger(__name__)
instance = None instance = None
port = 11435 port = 11435
data_dir = os.getenv("XDG_DATA_HOME")
overrides = {} overrides = {}
def start(): def start():
if not os.path.isdir(os.path.join(os.getenv("XDG_CACHE_HOME"), 'tmp/ollama')): if not os.path.isdir(os.path.join(cache_dir, 'tmp/ollama')):
os.mkdir(os.path.join(os.getenv("XDG_CACHE_HOME"), 'tmp/ollama')) os.mkdir(os.path.join(cache_dir, 'tmp/ollama'))
global instance, overrides global instance
params = overrides.copy() params = overrides.copy()
params["OLLAMA_HOST"] = f"127.0.0.1:{port}" # You can't change this directly sorry :3 params["OLLAMA_HOST"] = f"127.0.0.1:{port}" # You can't change this directly sorry :3
params["HOME"] = data_dir params["HOME"] = data_dir
params["TMPDIR"] = os.path.join(os.getenv("XDG_CACHE_HOME"), 'tmp/ollama') params["TMPDIR"] = os.path.join(cache_dir, 'tmp/ollama')
instance = subprocess.Popen(["/app/bin/ollama", "serve"], env={**os.environ, **params}, stderr=subprocess.PIPE, text=True) instance = subprocess.Popen(["ollama", "serve"], env={**os.environ, **params}, stderr=subprocess.PIPE, text=True)
logger.info("Starting Alpaca's Ollama instance...") logger.info("Starting Alpaca's Ollama instance...")
logger.debug(params) logger.debug(params)
sleep(1) sleep(1)
logger.info("Started Alpaca's Ollama instance") logger.info("Started Alpaca's Ollama instance")
v_str = subprocess.check_output("ollama -v", shell=True).decode('utf-8')
logger.info('Ollama version: {}'.format(v_str.split('client version is ')[1].strip()))
def stop(): def stop():
logger.info("Stopping Alpaca's Ollama instance") logger.info("Stopping Alpaca's Ollama instance")
@@ -39,4 +45,3 @@ def reset():
stop() stop()
sleep(1) sleep(1)
start() start()

View File

@@ -16,21 +16,35 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
"""
Main script run at launch, handles actions, about dialog and the app itself (not the window)
"""
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw, GLib
from .window import AlpacaWindow
from .internal import cache_dir, data_dir
import sys import sys
import logging import logging
import gi
import os import os
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw, GLib
from .window import AlpacaWindow
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
translators = [
'Alex K (Russian) https://github.com/alexkdeveloper',
'Jeffry Samuel (Spanish) https://github.com/jeffser',
'Louis Chauvet-Villaret (French) https://github.com/loulou64490',
'Théo FORTIN (French) https://github.com/topiga',
'Daimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein',
'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'
]
class AlpacaApplication(Adw.Application): class AlpacaApplication(Adw.Application):
"""The main application singleton class.""" """The main application singleton class."""
@@ -38,8 +52,8 @@ class AlpacaApplication(Adw.Application):
def __init__(self, version): def __init__(self, version):
super().__init__(application_id='com.jeffser.Alpaca', super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS) flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>q']) self.create_action('quit', lambda *_: self.quit(), ['<primary>w'])
self.create_action('preferences', lambda *_: AlpacaWindow.show_preferences_dialog(self.props.active_window), ['<primary>p']) self.create_action('preferences', lambda *_: AlpacaWindow.show_preferences_dialog(self.props.active_window), ['<primary>comma'])
self.create_action('about', self.on_about_action) self.create_action('about', self.on_about_action)
self.version = version self.version = version
@@ -58,12 +72,13 @@ class AlpacaApplication(Adw.Application):
support_url="https://github.com/Jeffser/Alpaca/discussions/155", support_url="https://github.com/Jeffser/Alpaca/discussions/155",
developers=['Jeffser https://jeffser.com'], developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com', 'Tobias Bernard (App Icon) https://tobiasbernard.com/'], designers=['Jeffser https://jeffser.com', 'Tobias Bernard (App Icon) https://tobiasbernard.com/'],
translator_credits='Alex K (Russian) https://github.com/alexkdeveloper\nJeffser (Spanish) https://jeffser.com\nDaimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein\nLouis Chauvet-Villaret (French) https://github.com/loulou64490\nCounterFlow64 (Norwegian) https://github.com/CounterFlow64\nAritra Saha (Bengali) https://github.com/olumolu\nYuehao Sui (Simplified Chinese) https://github.com/8ar10der', translator_credits='\n'.join(translators),
copyright='© 2024 Jeffser\n© 2024 Ollama', copyright='© 2024 Jeffser\n© 2024 Ollama',
issue_url='https://github.com/Jeffser/Alpaca/issues', issue_url='https://github.com/Jeffser/Alpaca/issues',
license_type=3, license_type=3,
website="https://jeffser.com/alpaca", website="https://jeffser.com/alpaca",
debug_info=open(os.path.join(os.getenv("XDG_DATA_HOME"), 'tmp.log'), 'r').read()) debug_info=open(os.path.join(data_dir, 'tmp.log'), 'r').read())
about.add_link("Become a Sponsor", "https://github.com/sponsors/Jeffser")
about.present(parent=self.props.active_window) about.present(parent=self.props.active_window)
def create_action(self, name, callback, shortcuts=None): def create_action(self, name, callback, shortcuts=None):
@@ -75,16 +90,16 @@ class AlpacaApplication(Adw.Application):
def main(version): def main(version):
if os.path.isfile(os.path.join(os.getenv("XDG_DATA_HOME"), 'tmp.log')): if os.path.isfile(os.path.join(data_dir, 'tmp.log')):
os.remove(os.path.join(os.getenv("XDG_DATA_HOME"), 'tmp.log')) os.remove(os.path.join(data_dir, 'tmp.log'))
if os.path.isdir(os.path.join(os.getenv("XDG_CACHE_HOME"), 'tmp')): if os.path.isdir(os.path.join(cache_dir, 'tmp')):
os.system('rm -rf ' + os.path.join(os.getenv("XDG_CACHE_HOME"), "tmp/*")) os.system('rm -rf ' + os.path.join(cache_dir, "tmp/*"))
else: else:
os.mkdir(os.path.join(os.getenv("XDG_CACHE_HOME"), 'tmp')) os.mkdir(os.path.join(cache_dir, 'tmp'))
logging.basicConfig( logging.basicConfig(
format="%(levelname)s\t[%(filename)s | %(funcName)s] %(message)s", format="%(levelname)s\t[%(filename)s | %(funcName)s] %(message)s",
level=logging.INFO, level=logging.INFO,
handlers=[logging.FileHandler(filename=os.path.join(os.getenv("XDG_DATA_HOME"), 'tmp.log')), logging.StreamHandler(stream=sys.stdout)] handlers=[logging.FileHandler(filename=os.path.join(data_dir, 'tmp.log')), logging.StreamHandler(stream=sys.stdout)]
) )
app = AlpacaApplication(version) app = AlpacaApplication(version)
logger.info(f"Alpaca version: {app.version}") logger.info(f"Alpaca version: {app.version}")

View File

@@ -44,7 +44,8 @@ alpaca_sources = [
'local_instance.py', 'local_instance.py',
'available_models.json', 'available_models.json',
'available_models_descriptions.py', 'available_models_descriptions.py',
'table_widget.py' 'table_widget.py',
'internal.py'
] ]
install_data(alpaca_sources, install_dir: moduledir) install_data(alpaca_sources, install_dir: moduledir)

View File

@@ -2,7 +2,7 @@
box-shadow: none; box-shadow: none;
border-width: 0; border-width: 0;
} }
.message_text_view { .message_text_view, .modelfile_textview {
background-color: rgba(0,0,0,0); background-color: rgba(0,0,0,0);
} }
.chat_image_button { .chat_image_button {
@@ -12,7 +12,3 @@
border-radius: 5px; border-radius: 5px;
padding: 5px; padding: 5px;
} }
.chat_row:selected {
background: mix(@theme_bg_color, @theme_selected_bg_color, 0.3);
color: mix(@window_fg_color, @theme_selected_bg_color, 0.5);
}

View File

@@ -1,5 +1,10 @@
#table_widget.py
"""
Handles the table widget shown in chat responses
"""
import gi import gi
from gi.repository import Adw gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GObject, Gio from gi.repository import Gtk, GObject, Gio
import re import re

View File

@@ -16,33 +16,37 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
"""
import gi Handles the main window
gi.require_version('GtkSource', '5') """
gi.require_version('GdkPixbuf', '2.0') import json, threading, os, re, base64, sys, gettext, uuid, shutil, tarfile, tempfile, logging
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
import json, requests, threading, os, re, base64, sys, gettext, locale, subprocess, uuid, shutil, tarfile, tempfile, logging, random
from time import sleep
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
from pypdf import PdfReader from pypdf import PdfReader
from datetime import datetime from datetime import datetime
import gi
gi.require_version('GtkSource', '5')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
from . import dialogs, local_instance, connection_handler, available_models_descriptions from . import dialogs, local_instance, connection_handler, available_models_descriptions
from .table_widget import TableWidget from .table_widget import TableWidget
from .internal import config_dir, data_dir, cache_dir, source_dir
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@Gtk.Template(resource_path='/com/jeffser/Alpaca/window.ui') @Gtk.Template(resource_path='/com/jeffser/Alpaca/window.ui')
class AlpacaWindow(Adw.ApplicationWindow): class AlpacaWindow(Adw.ApplicationWindow):
config_dir = os.getenv("XDG_CONFIG_HOME")
data_dir = os.getenv("XDG_DATA_HOME")
app_dir = os.getenv("FLATPAK_DEST") app_dir = os.getenv("FLATPAK_DEST")
cache_dir = os.getenv("XDG_CACHE_HOME") config_dir = config_dir
data_dir = data_dir
cache_dir = cache_dir
__gtype_name__ = 'AlpacaWindow' __gtype_name__ = 'AlpacaWindow'
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale') localedir = os.path.join(source_dir, 'locale')
gettext.bindtextdomain('com.jeffser.Alpaca', localedir) gettext.bindtextdomain('com.jeffser.Alpaca', localedir)
gettext.textdomain('com.jeffser.Alpaca') gettext.textdomain('com.jeffser.Alpaca')
@@ -60,7 +64,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
pulling_models = {} pulling_models = {}
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []} chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []}
attachments = {} attachments = {}
show_support = True
#Override elements #Override elements
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child() override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
@@ -68,11 +71,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
override_HIP_VISIBLE_DEVICES = Gtk.Template.Child() override_HIP_VISIBLE_DEVICES = Gtk.Template.Child()
#Elements #Elements
split_view_overlay = Gtk.Template.Child()
regenerate_button : Gtk.Button = None
selected_chat_row : Gtk.ListBoxRow = None
create_model_base = Gtk.Template.Child() create_model_base = Gtk.Template.Child()
create_model_name = Gtk.Template.Child() create_model_name = Gtk.Template.Child()
create_model_system = Gtk.Template.Child() create_model_system = Gtk.Template.Child()
create_model_template = Gtk.Template.Child() create_model_modelfile = Gtk.Template.Child()
create_model_dialog = Gtk.Template.Child()
temperature_spin = Gtk.Template.Child() temperature_spin = Gtk.Template.Child()
seed_spin = Gtk.Template.Child() seed_spin = Gtk.Template.Child()
keep_alive_spin = Gtk.Template.Child() keep_alive_spin = Gtk.Template.Child()
@@ -134,14 +139,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback() @Gtk.Template.Callback()
def verify_if_image_can_be_used(self, pspec=None, user_data=None): def verify_if_image_can_be_used(self, pspec=None, user_data=None):
logger.debug("Verifying if image can be used") logger.debug("Verifying if image can be used")
if self.model_drop_down.get_selected_item() == None: return True if self.model_drop_down.get_selected_item() == None:
selected = self.model_drop_down.get_selected_item().get_string().split(" (")[0].lower() 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"]]: if selected in [key for key, value in self.available_models.items() if value["image"]]:
for name, content in self.attachments.items(): for name, content in self.attachments.items():
if content['type'] == 'image': if content['type'] == 'image':
content['button'].set_css_classes(["flat"]) content['button'].set_css_classes(["flat"])
return True return True
else:
for name, content in self.attachments.items(): for name, content in self.attachments.items():
if content['type'] == 'image': if content['type'] == 'image':
content['button'].set_css_classes(["flat", "error"]) content['button'].set_css_classes(["flat", "error"])
@@ -149,7 +154,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback() @Gtk.Template.Callback()
def stop_message(self, button=None): def stop_message(self, button=None):
if self.loading_spinner: self.chat_container.remove(self.loading_spinner) if self.loading_spinner:
self.chat_container.remove(self.loading_spinner)
self.toggle_ui_sensitive(True) self.toggle_ui_sensitive(True)
self.switch_send_stop_button() self.switch_send_stop_button()
self.bot_message = None self.bot_message = None
@@ -173,8 +179,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.save_history() self.save_history()
self.show_toast(_("Message edited successfully"), self.main_overlay) 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): return if self.bot_message or self.get_focus() not in (self.message_text_view, self.send_button):
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 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
current_chat_row = self.chat_list_box.get_selected_row() current_chat_row = self.chat_list_box.get_selected_row()
self.chat_list_box.unselect_all() self.chat_list_box.unselect_all()
self.chat_list_box.remove(current_chat_row) self.chat_list_box.remove(current_chat_row)
@@ -183,23 +191,23 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats['order'].remove(self.chats['selected_chat']) self.chats['order'].remove(self.chats['selected_chat'])
self.chats['order'].insert(0, self.chats['selected_chat']) self.chats['order'].insert(0, self.chats['selected_chat'])
self.save_history() self.save_history()
current_model = self.model_drop_down.get_selected_item().get_string().split(' (') current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
current_model = '{}:{}'.format(current_model[0].replace(' ', '-').lower(), current_model[1][:-1])
if current_model is None: if current_model is None:
self.show_toast(_("Please select a model before chatting"), self.main_overlay) self.show_toast(_("Please select a model before chatting"), self.main_overlay)
return return
id = self.generate_uuid() message_id = self.generate_uuid()
attached_images = [] attached_images = []
attached_files = {} attached_files = {}
can_use_images = self.verify_if_image_can_be_used() can_use_images = self.verify_if_image_can_be_used()
for name, content in self.attachments.items(): for name, content in self.attachments.items():
if content["type"] == 'image' and can_use_images: attached_images.append(name) if content["type"] == 'image' and can_use_images:
attached_images.append(name)
else: else:
attached_files[name] = content['type'] attached_files[name] = content['type']
if not os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)): if not os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id)):
os.makedirs(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)) os.makedirs(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id))
shutil.copy(content['path'], os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)) shutil.copy(content['path'], os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id, name))
content["button"].get_parent().remove(content["button"]) content["button"].get_parent().remove(content["button"])
self.attachments = {} self.attachments = {}
self.attachment_box.set_visible(False) self.attachment_box.set_visible(False)
@@ -208,16 +216,16 @@ class AlpacaWindow(Adw.ApplicationWindow):
current_datetime = datetime.now() current_datetime = datetime.now()
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = { self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id] = {
"role": "user", "role": "user",
"model": "User", "model": "User",
"date": current_datetime.strftime("%Y/%m/%d %H:%M:%S"), "date": current_datetime.strftime("%Y/%m/%d %H:%M:%S"),
"content": 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) "content": 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)
} }
if len(attached_images) > 0: if len(attached_images) > 0:
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['images'] = attached_images self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]['images'] = attached_images
if len(attached_files.keys()) > 0: if len(attached_files.keys()) > 0:
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['files'] = attached_files self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]['files'] = attached_files
data = { data = {
"model": current_model, "model": current_model,
"messages": self.convert_history_to_ollama(), "messages": self.convert_history_to_ollama(),
@@ -230,12 +238,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
#self.attachments[name] = {"path": file_path, "type": file_type, "content": content} #self.attachments[name] = {"path": file_path, "type": file_type, "content": content}
raw_message = 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) raw_message = 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)
formated_date = GLib.markup_escape_text(self.generate_datetime_format(current_datetime)) formated_date = GLib.markup_escape_text(self.generate_datetime_format(current_datetime))
self.show_message(raw_message, False, f"\n\n<small>{formated_date}</small>", attached_images, attached_files, id=id) self.show_message(raw_message, False, f"\n\n<small>{formated_date}</small>", attached_images, attached_files, message_id=message_id)
self.message_text_view.get_buffer().set_text("", 0) self.message_text_view.get_buffer().set_text("", 0)
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True) self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
self.chat_container.append(self.loading_spinner) self.chat_container.append(self.loading_spinner)
bot_id=self.generate_uuid() bot_id=self.generate_uuid()
self.show_message("", True, id=bot_id) self.show_message("", True, message_id=bot_id)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], bot_id)) thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], bot_id))
thread.start() thread.start()
@@ -245,17 +253,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
generate_title_thread = threading.Thread(target=self.generate_chat_title, args=(message_data, self.chat_list_box.get_selected_row().get_child())) generate_title_thread = threading.Thread(target=self.generate_chat_title, args=(message_data, self.chat_list_box.get_selected_row().get_child()))
generate_title_thread.start() generate_title_thread.start()
@Gtk.Template.Callback()
def manage_models_button_activate(self, button=None):
logger.debug(f"Managing models")
self.update_list_local_models()
self.manage_models_dialog.present(self)
@Gtk.Template.Callback() @Gtk.Template.Callback()
def welcome_carousel_page_changed(self, carousel, index): def welcome_carousel_page_changed(self, carousel, index):
logger.debug("Showing welcome carousel") logger.debug("Showing welcome carousel")
if index == 0: self.welcome_previous_button.set_sensitive(False) if index == 0:
else: self.welcome_previous_button.set_sensitive(True) self.welcome_previous_button.set_sensitive(False)
else:
self.welcome_previous_button.set_sensitive(True)
if index == carousel.get_n_pages()-1: if index == carousel.get_n_pages()-1:
self.welcome_next_button.set_label(_("Close")) self.welcome_next_button.set_label(_("Close"))
self.welcome_next_button.set_tooltip_text(_("Close")) self.welcome_next_button.set_tooltip_text(_("Close"))
@@ -269,7 +273,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback() @Gtk.Template.Callback()
def welcome_next_button_activate(self, button): def welcome_next_button_activate(self, button):
if button.get_label() == "Next": self.welcome_carousel.scroll_to(self.welcome_carousel.get_nth_page(self.welcome_carousel.get_position()+1), True) if button.get_label() == "Next":
self.welcome_carousel.scroll_to(self.welcome_carousel.get_nth_page(self.welcome_carousel.get_position()+1), True)
else: else:
self.welcome_dialog.force_close() self.welcome_dialog.force_close()
if not self.verify_connection(): if not self.verify_connection():
@@ -282,8 +287,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats["selected_chat"] = row.get_child().get_name() self.chats["selected_chat"] = row.get_child().get_name()
self.load_history_into_chat() 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"].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()): for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]: if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i) self.model_drop_down.set_selected(i)
break break
self.save_history() self.save_history()
@@ -335,27 +342,28 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback() @Gtk.Template.Callback()
def model_spin_changed(self, spin): def model_spin_changed(self, spin):
value = spin.get_value() value = spin.get_value()
if spin.get_name() != "temperature": value = round(value) if spin.get_name() != "temperature":
else: value = round(value, 1) value = round(value)
else:
value = round(value, 1)
if self.model_tweaks[spin.get_name()] is not None and self.model_tweaks[spin.get_name()] != value: if self.model_tweaks[spin.get_name()] is not None and self.model_tweaks[spin.get_name()] != value:
self.model_tweaks[spin.get_name()] = value self.model_tweaks[spin.get_name()] = value
self.save_server_config() self.save_server_config()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def create_model_start(self, button): def create_model_start(self, button):
base = self.create_model_base.get_subtitle() name = self.create_model_name.get_text().lower().replace(":", "")
name = self.create_model_name.get_text() modelfile_buffer = self.create_model_modelfile.get_buffer()
system = self.create_model_system.get_text() modelfile_raw = modelfile_buffer.get_text(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter(), False)
template = self.create_model_template.get_text() modelfile = ["FROM {}".format(self.create_model_base.get_subtitle()), "SYSTEM {}".format(self.create_model_system.get_text())]
if "/" in base: for line in modelfile_raw.split('\n'):
modelfile = f"FROM {base}\nSYSTEM {system}\nTEMPLATE {template}" if not line.startswith('SYSTEM') and not line.startswith('FROM'):
else: modelfile.append(line)
modelfile = f"FROM {base}\nSYSTEM {system}"
self.pulling_model_list_box.set_visible(True) self.pulling_model_list_box.set_visible(True)
model_row = Adw.ActionRow( model_row = Adw.ActionRow(
title = name title = name
) )
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": name, "modelfile": modelfile}) thread = threading.Thread(target=self.pull_model_process, kwargs={"model": name, "modelfile": '\n'.join(modelfile)})
overlay = Gtk.Overlay() overlay = Gtk.Overlay()
progress_bar = Gtk.ProgressBar( progress_bar = Gtk.ProgressBar(
valign = 2, valign = 2,
@@ -377,19 +385,22 @@ class AlpacaWindow(Adw.ApplicationWindow):
overlay.set_child(model_row) overlay.set_child(model_row)
overlay.add_overlay(progress_bar) overlay.add_overlay(progress_bar)
self.pulling_model_list_box.append(overlay) self.pulling_model_list_box.append(overlay)
self.create_model_dialog.close() self.navigation_view_manage_models.pop()
self.manage_models_dialog.present(self)
thread.start() thread.start()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def override_changed(self, entry): def override_changed(self, entry):
name = entry.get_name() name = entry.get_name()
value = entry.get_text() value = entry.get_text()
if (not value and name not in local_instance.overrides) or (value and value in local_instance.overrides and local_instance.overrides[name] == value): return if (not value and name not in local_instance.overrides) or (value and value in local_instance.overrides and local_instance.overrides[name] == value):
if not value: del local_instance.overrides[name] return
else: local_instance.overrides[name] = value if not value:
del local_instance.overrides[name]
else:
local_instance.overrides[name] = value
self.save_server_config() self.save_server_config()
if not self.run_remote: local_instance.reset() if not self.run_remote:
local_instance.reset()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def link_button_handler(self, button): def link_button_handler(self, button):
@@ -407,7 +418,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
for i, key in enumerate(self.available_models.keys()): for i, key in enumerate(self.available_models.keys()):
row = self.available_model_list_box.get_row_at_index(i) row = self.available_model_list_box.get_row_at_index(i)
row.set_visible(re.search(entry.get_text(), '{} {} {}'.format(row.get_title(), (_("image") if self.available_models[key]['image'] else " "), row.get_subtitle()), re.IGNORECASE)) row.set_visible(re.search(entry.get_text(), '{} {} {}'.format(row.get_title(), (_("image") if self.available_models[key]['image'] else " "), row.get_subtitle()), re.IGNORECASE))
if row.get_visible(): results += 1 if row.get_visible():
results += 1
if entry.get_text() and results == 0: if entry.get_text() and results == 0:
self.available_model_list_box.set_visible(False) self.available_model_list_box.set_visible(False)
self.no_results_page.set_visible(True) self.no_results_page.set_visible(True)
@@ -415,38 +427,53 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.available_model_list_box.set_visible(True) self.available_model_list_box.set_visible(True)
self.no_results_page.set_visible(False) 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)
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])
def check_alphanumeric(self, editable, text, length, position): def check_alphanumeric(self, editable, text, length, position):
new_text = ''.join([char for char in text if char.isalnum() or char in ['-', '_']]) new_text = ''.join([char for char in text if char.isalnum() or char in ['-', '.', ':', '_']])
if new_text != text: editable.stop_emission_by_name("insert-text") if new_text != text:
editable.stop_emission_by_name("insert-text")
def create_model(self, model:str, file:bool): def create_model(self, model:str, file:bool):
name = "" modelfile_buffer = self.create_model_modelfile.get_buffer()
system = "" modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter())
template = "" self.create_model_system.set_text('')
if not file: if not file:
response = connection_handler.simple_post(f"{connection_handler.url}/api/show", json.dumps({"name": model})) 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: if response.status_code == 200:
data = json.loads(response.text) data = json.loads(response.text)
modelfile = []
for line in data['modelfile'].split('\n'): for line in data['modelfile'].split('\n'):
if line.startswith('SYSTEM'): if line.startswith('SYSTEM'):
system = line[len('SYSTEM'):].strip() self.create_model_system.set_text(line[len('SYSTEM'):].strip())
elif line.startswith('TEMPLATE'): if not line.startswith('SYSTEM') and not line.startswith('FROM') and not line.startswith('#'):
template = line[len('TEMPLATE'):].strip() modelfile.append(line)
self.create_model_template.set_sensitive(False) self.create_model_name.set_text(self.convert_model_name(model, 1).split(':')[0] + "-custom")
name = model.split(':')[0] modelfile_buffer.insert(modelfile_buffer.get_start_iter(), '\n'.join(modelfile), len('\n'.join(modelfile).encode('utf-8')))
else: else:
self.create_model_template.set_sensitive(True) ##TODO ERROR MESSAGE
template = '"""{{ if .System }}<|start_header_id|>system<|end_header_id|>\n\n{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>\n\n{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>\n{{ .Response }}<|eot_id|>"""' return
name = model.split("/")[-1].split(".")[0] self.create_model_base.set_subtitle(self.convert_model_name(model, 1))
else:
self.create_model_name.set_text(os.path.splitext(os.path.basename(model))[0])
self.create_model_base.set_subtitle(model) self.create_model_base.set_subtitle(model)
self.create_model_name.set_text(name) self.navigation_view_manage_models.push_by_tag('model_create_page')
self.create_model_system.set_text(system)
self.create_model_template.set_text(template)
self.manage_models_dialog.close()
self.create_model_dialog.present(self)
def show_toast(self, message:str, overlay): def show_toast(self, message:str, overlay):
logger.info(message) logger.info(message)
@@ -461,31 +488,33 @@ class AlpacaWindow(Adw.ApplicationWindow):
logger.info(f"{title}, {body}") logger.info(f"{title}, {body}")
notification = Gio.Notification.new(title) notification = Gio.Notification.new(title)
notification.set_body(body) notification.set_body(body)
if icon: notification.set_icon(icon) if icon:
notification.set_icon(icon)
self.get_application().send_notification(None, notification) self.get_application().send_notification(None, notification)
def delete_message(self, message_element): def delete_message(self, message_element):
logger.debug("Deleting message") logger.debug("Deleting message")
id = message_element.get_name() message_id = message_element.get_name()
del self.chats["chats"][self.chats["selected_chat"]]["messages"][id] del self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]
self.chat_container.remove(message_element) self.chat_container.remove(message_element)
if os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)): 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'], id)) shutil.rmtree(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id))
self.save_history() self.save_history()
def copy_message(self, message_element): def copy_message(self, message_element):
logger.debug("Copying message") logger.debug("Copying message")
id = message_element.get_name() message_id = message_element.get_name()
clipboard = Gdk.Display().get_default().get_clipboard() clipboard = Gdk.Display().get_default().get_clipboard()
clipboard.set(self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["content"]) clipboard.set(self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["content"])
self.show_toast(_("Message copied to the clipboard"), self.main_overlay) self.show_toast(_("Message copied to the clipboard"), self.main_overlay)
def edit_message(self, message_element, text_view, button_container): def edit_message(self, message_element, text_view, button_container):
logger.debug("Editing message") logger.debug("Editing message")
if self.editing_message: self.send_message() if self.editing_message:
self.send_message()
button_container.set_visible(False) button_container.set_visible(False)
id = message_element.get_name() message_id = message_element.get_name()
text_buffer = text_view.get_buffer() text_buffer = text_view.get_buffer()
end_iter = text_buffer.get_end_iter() end_iter = text_buffer.get_end_iter()
@@ -499,7 +528,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
text_view.set_css_classes(["view", "editing_message_textview"]) text_view.set_css_classes(["view", "editing_message_textview"])
text_view.set_cursor_visible(True) text_view.set_cursor_visible(True)
self.editing_message = {"text_view": text_view, "id": id, "button_container": button_container, "footer": footer} self.editing_message = {"text_view": text_view, "id": message_id, "button_container": button_container, "footer": footer}
def preview_file(self, file_path, file_type, presend_name): def preview_file(self, file_path, file_type, presend_name):
logger.debug(f"Previewing file: {file_path}") logger.debug(f"Previewing file: {file_path}")
@@ -542,22 +571,24 @@ class AlpacaWindow(Adw.ApplicationWindow):
def convert_history_to_ollama(self): def convert_history_to_ollama(self):
messages = [] messages = []
for id, message in self.chats["chats"][self.chats["selected_chat"]]["messages"].items(): for message_id, message in self.chats["chats"][self.chats["selected_chat"]]["messages"].items():
new_message = message.copy() new_message = message.copy()
if 'files' in message and len(message['files']) > 0: if 'files' in message and len(message['files']) > 0:
del new_message['files'] del new_message['files']
new_message['content'] = '' new_message['content'] = ''
for name, file_type in message['files'].items(): for name, file_type in message['files'].items():
file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name) file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id, name)
file_data = self.get_content_of_file(file_path, file_type) file_data = self.get_content_of_file(file_path, file_type)
if file_data: new_message['content'] += f"```[{name}]\n{file_data}\n```" if file_data:
new_message['content'] += f"```[{name}]\n{file_data}\n```"
new_message['content'] += message['content'] new_message['content'] += message['content']
if 'images' in message and len(message['images']) > 0: if 'images' in message and len(message['images']) > 0:
new_message['images'] = [] new_message['images'] = []
for name in message['images']: for name in message['images']:
file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name) file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id, name)
image_data = self.get_content_of_file(file_path, 'image') image_data = self.get_content_of_file(file_path, 'image')
if image_data: new_message['images'].append(image_data) if image_data:
new_message['images'].append(image_data)
messages.append(new_message) messages.append(new_message)
return messages return messages
@@ -574,17 +605,17 @@ Generate a title following these rules:
```PROMPT ```PROMPT
{message['content']} {message['content']}
```""" ```"""
current_model = self.model_drop_down.get_selected_item().get_string().split(' (') current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
current_model = '{}:{}'.format(current_model[0].replace(' ', '-').lower(), current_model[1][:-1])
data = {"model": current_model, "prompt": prompt, "stream": False} data = {"model": current_model, "prompt": prompt, "stream": False}
if 'images' in message: data["images"] = message['images'] 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() 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 '...') new_chat_name = new_chat_name[:50] + (new_chat_name[50:] and '...')
self.rename_chat(label_element.get_name(), new_chat_name, label_element) self.rename_chat(label_element.get_name(), new_chat_name, label_element)
def show_message(self, msg:str, bot:bool, footer:str=None, images:list=None, files:dict=None, id:str=None): def show_message(self, msg:str, bot:bool, footer:str=None, images:list=None, files:dict=None, message_id:str=None):
message_text = Gtk.TextView( message_text = Gtk.TextView(
editable=False, editable=False,
focusable=True, focusable=True,
@@ -599,7 +630,8 @@ Generate a title following these rules:
) )
message_buffer = message_text.get_buffer() message_buffer = message_text.get_buffer()
message_buffer.insert(message_buffer.get_end_iter(), msg) message_buffer.insert(message_buffer.get_end_iter(), msg)
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8'))) if footer is not None:
message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
delete_button = Gtk.Button( delete_button = Gtk.Button(
icon_name = "user-trash-symbolic", icon_name = "user-trash-symbolic",
@@ -616,6 +648,11 @@ Generate a title following these rules:
css_classes = ["flat", "circular"], css_classes = ["flat", "circular"],
tooltip_text = _("Edit Message") tooltip_text = _("Edit Message")
) )
regenerate_button = Gtk.Button(
icon_name = "update-symbolic",
css_classes = ["flat", "circular"],
tooltip_text = _("Regenerate Message")
)
button_container = Gtk.Box( button_container = Gtk.Box(
orientation=0, orientation=0,
@@ -647,7 +684,7 @@ Generate a title following these rules:
child=image_container child=image_container
) )
for image in images: for image in images:
path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, image) path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], message_id, image)
try: try:
if not os.path.isfile(path): if not os.path.isfile(path):
raise FileNotFoundError("'{}' was not found or is a directory".format(path)) raise FileNotFoundError("'{}' was not found or is a directory".format(path))
@@ -656,8 +693,8 @@ Generate a title following these rules:
button = Gtk.Button( button = Gtk.Button(
child=image_element, child=image_element,
css_classes=["flat", "chat_image_button"], css_classes=["flat", "chat_image_button"],
name=os.path.join(self.data_dir, "chats", "{selected_chat}", id, image), name=os.path.join(self.data_dir, "chats", "{selected_chat}", message_id, image),
tooltip_text=os.path.basename(path) tooltip_text=_("Image")
) )
button.connect("clicked", lambda button, file_path=path: self.preview_file(file_path, 'image', None)) button.connect("clicked", lambda button, file_path=path: self.preview_file(file_path, 'image', None))
except Exception as e: except Exception as e:
@@ -683,7 +720,7 @@ Generate a title following these rules:
button = Gtk.Button( button = Gtk.Button(
child=image_box, child=image_box,
css_classes=["flat", "chat_image_button"], css_classes=["flat", "chat_image_button"],
tooltip_text=_("Missing image") tooltip_text=_("Missing Image")
) )
button.connect("clicked", lambda button : self.show_toast(_("Missing image"), self.main_overlay)) button.connect("clicked", lambda button : self.show_toast(_("Missing image"), self.main_overlay))
image_container.append(button) image_container.append(button)
@@ -719,21 +756,22 @@ Generate a title following these rules:
tooltip_text=name, tooltip_text=name,
child=button_content child=button_content
) )
file_path = os.path.join(self.data_dir, "chats", "{selected_chat}", id, name) file_path = os.path.join(self.data_dir, "chats", "{selected_chat}", message_id, name)
button.connect("clicked", lambda button, file_path=file_path, file_type=file_type: self.preview_file(file_path, file_type, None)) button.connect("clicked", lambda button, file_path=file_path, file_type=file_type: self.preview_file(file_path, file_type, None))
file_container.append(button) file_container.append(button)
message_box.append(file_scroller) message_box.append(file_scroller)
message_box.append(message_text) message_box.append(message_text)
overlay = Gtk.Overlay(css_classes=["message"], name=id) overlay = Gtk.Overlay(css_classes=["message"], name=message_id)
overlay.set_child(message_box) overlay.set_child(message_box)
delete_button.connect("clicked", lambda button, element=overlay: self.delete_message(element)) delete_button.connect("clicked", lambda button, element=overlay: self.delete_message(element))
copy_button.connect("clicked", lambda button, element=overlay: self.copy_message(element)) copy_button.connect("clicked", lambda button, element=overlay: self.copy_message(element))
edit_button.connect("clicked", lambda button, element=overlay, textview=message_text, button_container=button_container: self.edit_message(element, textview, button_container)) edit_button.connect("clicked", lambda button, element=overlay, textview=message_text, button_container=button_container: self.edit_message(element, textview, button_container))
regenerate_button.connect('clicked', lambda button, message_id=message_id, bot_message_box=message_box, bot_message_button_container=button_container : self.regenerate_message(message_id, bot_message_box, bot_message_button_container))
button_container.append(delete_button) button_container.append(delete_button)
button_container.append(copy_button) button_container.append(copy_button)
if not bot: button_container.append(edit_button) button_container.append(regenerate_button if bot else edit_button)
overlay.add_overlay(button_container) overlay.add_overlay(button_container)
self.chat_container.append(overlay) self.chat_container.append(overlay)
@@ -756,32 +794,31 @@ Generate a title following these rules:
else: else:
self.local_model_list_box.set_visible(True) self.local_model_list_box.set_visible(True)
for model in json.loads(response.text)['models']: for model in json.loads(response.text)['models']:
model_name = self.convert_model_name(model["name"], 0)
model_row = Adw.ActionRow( model_row = Adw.ActionRow(
title = "<b>{}</b>".format(model["name"].split(":")[0].replace("-", " ").title()), title = "<b>{}</b>".format(model_name.split(" (")[0]),
subtitle = model["name"].split(":")[1] subtitle = model_name.split(" (")[1][:-1]
) )
button = Gtk.Button( button = Gtk.Button(
icon_name = "user-trash-symbolic", icon_name = "user-trash-symbolic",
vexpand = False, vexpand = False,
valign = 3, valign = 3,
css_classes = ["error", "circular"], css_classes = ["error", "circular"],
tooltip_text = _("Remove '{} ({})'").format(model["name"].split(":")[0].replace('-', ' ').title(), model["name"].split(":")[1]) tooltip_text = _("Remove '{}'").format(model_name)
) )
button.connect("clicked", lambda button=button, model_name=model["name"]: dialogs.delete_model(self, model_name)) button.connect("clicked", lambda button=button, model_name=model["name"]: dialogs.delete_model(self, model_name))
model_row.add_suffix(button) model_row.add_suffix(button)
self.local_model_list_box.append(model_row) self.local_model_list_box.append(model_row)
self.model_string_list.append(f"{model['name'].split(':')[0].replace('-', ' ').title()} ({model['name'].split(':')[1]})") self.model_string_list.append(model_name)
self.local_models.append(model["name"]) self.local_models.append(model["name"])
self.model_drop_down.set_selected(0) #self.verify_if_image_can_be_used()
self.verify_if_image_can_be_used()
return
else: else:
self.connection_error() self.connection_error()
def save_server_config(self): def save_server_config(self):
with open(os.path.join(self.config_dir, "server.json"), "w+") as f: with open(os.path.join(self.config_dir, "server.json"), "w+", encoding="utf-8") as f:
json.dump({'remote_url': self.remote_url, 'remote_bearer_token': self.remote_bearer_token, 'run_remote': self.run_remote, 'local_port': local_instance.port, 'run_on_background': self.run_on_background, 'model_tweaks': self.model_tweaks, 'ollama_overrides': local_instance.overrides, 'show_support': self.show_support}, f, indent=6) json.dump({'remote_url': self.remote_url, 'remote_bearer_token': self.remote_bearer_token, 'run_remote': self.run_remote, 'local_port': local_instance.port, 'run_on_background': self.run_on_background, 'model_tweaks': self.model_tweaks, 'ollama_overrides': local_instance.overrides}, f, indent=6)
def verify_connection(self): def verify_connection(self):
try: try:
@@ -937,20 +974,22 @@ Generate a title following these rules:
def generate_datetime_format(self, dt:datetime) -> str: def generate_datetime_format(self, dt:datetime) -> str:
date = GLib.DateTime.new(GLib.DateTime.new_now_local().get_timezone(), dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) date = GLib.DateTime.new(GLib.DateTime.new_now_local().get_timezone(), dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
current_date = GLib.DateTime.new_now_local() current_date = GLib.DateTime.new_now_local()
if date.format("%Y/%m/%d") == current_date.format("%Y/%m/%d"): return date.format("%H:%M %p") if date.format("%Y/%m/%d") == current_date.format("%Y/%m/%d"):
elif date.format("%Y") == current_date.format("%Y"): return date.format("%b %d, %H:%M %p") return date.format("%H:%M %p")
else: return date.format("%b %d %Y, %H:%M %p") if date.format("%Y") == current_date.format("%Y"):
return date.format("%b %d, %H:%M %p")
return date.format("%b %d %Y, %H:%M %p")
def update_bot_message(self, data, id): def update_bot_message(self, data, message_id):
if self.bot_message is None: if self.bot_message is None:
self.save_history() self.save_history()
sys.exit() sys.exit()
vadjustment = self.chat_window.get_vadjustment() vadjustment = self.chat_window.get_vadjustment()
if id not in self.chats["chats"][self.chats["selected_chat"]]["messages"] or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size(): 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():
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper()) GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
if data['done']: if 'done' in data and data['done']:
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["date"], '%Y/%m/%d %H:%M:%S'))) 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{data['model'].split(':')[0].replace('-', ' ').title()} ({data['model'].split(':')[1]})\n<small>{formated_date}</small>" text = f"\n\n{self.convert_model_name(data['model'], 0)}\n<small>{formated_date}</small>"
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text.encode('utf-8'))) GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text.encode('utf-8')))
self.save_history() self.save_history()
GLib.idle_add(self.bot_message_button_container.set_visible, True) GLib.idle_add(self.bot_message_button_container.set_visible, True)
@@ -958,17 +997,11 @@ Generate a title following these rules:
first_paragraph = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), False).split("\n")[0] first_paragraph = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), False).split("\n")[0]
GLib.idle_add(self.show_notification, self.chats["selected_chat"], first_paragraph[:100] + (first_paragraph[100:] and '...'), Gio.ThemedIcon.new("chat-message-new-symbolic")) GLib.idle_add(self.show_notification, self.chats["selected_chat"], first_paragraph[:100] + (first_paragraph[100:] and '...'), Gio.ThemedIcon.new("chat-message-new-symbolic"))
else: else:
if id not in self.chats["chats"][self.chats["selected_chat"]]["messages"]: if not self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]["content"] and self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None self.loading_spinner = None
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
"role": "assistant",
"model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"content": ''
}
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content']) GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['content'] += data['message']['content'] self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id]['content'] += data['message']['content']
def toggle_ui_sensitive(self, status): def toggle_ui_sensitive(self, status):
for element in [self.chat_list_box, self.add_chat_button, self.secondary_menu_button]: for element in [self.chat_list_box, self.add_chat_button, self.secondary_menu_button]:
@@ -978,21 +1011,75 @@ Generate a title following these rules:
self.stop_button.set_visible(self.send_button.get_visible()) self.stop_button.set_visible(self.send_button.get_visible())
self.send_button.set_visible(not self.send_button.get_visible()) self.send_button.set_visible(not self.send_button.get_visible())
def run_message(self, messages, model, id): def run_message(self, messages, model, message_id):
logger.debug("Running message") logger.debug("Running message")
self.bot_message_button_container.set_visible(False) self.bot_message_button_container.set_visible(False)
response = connection_handler.stream_post(f"{connection_handler.url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=lambda data, id=id: self.update_bot_message(data, id)) self.chats["chats"][self.chats["selected_chat"]]["messages"][message_id] = {
"role": "assistant",
"model": model,
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"content": ''
}
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))
if response.status_code != 200:
raise Exception('Network Error')
GLib.idle_add(self.add_code_blocks) GLib.idle_add(self.add_code_blocks)
except Exception as e:
GLib.idle_add(self.connection_error)
self.regenerate_button = Gtk.Button(
child=Adw.ButtonContent(
icon_name='update-symbolic',
label=_('Regenerate Response')
),
css_classes=["suggested-action"],
halign=3
)
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)
GLib.idle_add(self.toggle_ui_sensitive, True) GLib.idle_add(self.toggle_ui_sensitive, True)
if self.loading_spinner: if self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None self.loading_spinner = None
if response.status_code != 200:
GLib.idle_add(self.connection_error) 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()
def pull_model_update(self, data, model_name): def pull_model_update(self, data, model_name):
if model_name in list(self.pulling_models.keys()): if 'error' in data:
self.pulling_models[model_name]['error'] = data['error']
return
if model_name in self.pulling_models.keys():
if 'completed' in data and 'total' in data: if 'completed' in data and 'total' in data:
GLib.idle_add(self.pulling_models[model_name]['row'].set_subtitle, '<tt>{}%</tt>'.format(round(data['completed'] / data['total'] * 100, 2))) GLib.idle_add(self.pulling_models[model_name]['row'].set_subtitle, '<tt>{}%</tt>'.format(round(data['completed'] / data['total'] * 100, 2)))
GLib.idle_add(self.pulling_models[model_name]['progress_bar'].set_fraction, (data['completed'] / data['total'])) GLib.idle_add(self.pulling_models[model_name]['progress_bar'].set_fraction, (data['completed'] / data['total']))
@@ -1012,28 +1099,32 @@ Generate a title following these rules:
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.update_list_local_models)
if response.status_code == 200: 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")) GLib.idle_add(self.show_notification, _("Task Complete"), _("Model '{}' pulled successfully.").format(model), Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(self.show_toast, _("Model '{}' pulled successfully.").format(model), self.manage_models_overlay) GLib.idle_add(self.show_toast, _("Model '{}' pulled successfully.").format(model), self.manage_models_overlay)
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent()) elif response.status_code == 200 and self.pulling_models[model]['error']:
del self.pulling_models[model] GLib.idle_add(self.show_notification, _("Pull Model Error"), _("Failed to pull model '{}': {}").format(model, self.pulling_models[model]['error']), Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.show_toast, _("Error pulling '{}': {}").format(model, self.pulling_models[model]['error']), self.manage_models_overlay)
else: else:
GLib.idle_add(self.show_notification, _("Pull Model Error"), _("Failed to pull model '{}' due to network error.").format(model), Gio.ThemedIcon.new("dialog-error-symbolic")) GLib.idle_add(self.show_notification, _("Pull Model Error"), _("Failed to pull model '{}' due to network error.").format(model), Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent()) GLib.idle_add(self.show_toast, _("Error pulling '{}'").format(model), self.manage_models_overlay)
del self.pulling_models[model]
GLib.idle_add(self.manage_models_dialog.close) GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.connection_error) GLib.idle_add(self.connection_error)
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent())
del self.pulling_models[model]
if len(list(self.pulling_models.keys())) == 0: if len(list(self.pulling_models.keys())) == 0:
GLib.idle_add(self.pulling_model_list_box.set_visible, False) GLib.idle_add(self.pulling_model_list_box.set_visible, False)
def pull_model(self, model): def pull_model(self, model):
logger.info("Pulling model") if model in self.pulling_models.keys() or model in self.local_models or ":" not in model:
if model in list(self.pulling_models.keys()) or model in self.local_models:
return return
logger.info("Pulling model")
self.pulling_model_list_box.set_visible(True) self.pulling_model_list_box.set_visible(True)
#self.pulling_model_list_box.connect('row_selected', lambda list_box, row: dialogs.stop_pull_model(self, row.get_name()) if row else None) #It isn't working for some reason #self.pulling_model_list_box.connect('row_selected', lambda list_box, row: dialogs.stop_pull_model(self, row.get_name()) if row else None) #It isn't working for some reason
model_name = self.convert_model_name(model, 0)
model_row = Adw.ActionRow( model_row = Adw.ActionRow(
title = "<b>{}</b> <small>{}</small>".format(model.split(":")[0].replace("-", " ").title(), model.split(":")[1]), title = "<b>{}</b> <small>{}</small>".format(model_name.split(" (")[0], model_name.split(" (")[1][:-1]),
name = model name = model
) )
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": model, "modelfile": None}) thread = threading.Thread(target=self.pull_model_process, kwargs={"model": model, "modelfile": None})
@@ -1050,7 +1141,7 @@ Generate a title following these rules:
vexpand = False, vexpand = False,
valign = 3, valign = 3,
css_classes = ["error", "circular"], css_classes = ["error", "circular"],
tooltip_text = _("Stop Pulling '{} ({})'").format(model.split(':')[0].replace('-', ' ').title(), model.split(':')[1]) tooltip_text = _("Stop Pulling '{}'").format(model_name)
) )
button.connect("clicked", lambda button, model_name=model : dialogs.stop_pull_model(self, model_name)) button.connect("clicked", lambda button, model_name=model : dialogs.stop_pull_model(self, model_name))
model_row.add_suffix(button) model_row.add_suffix(button)
@@ -1107,7 +1198,7 @@ Generate a title following these rules:
def save_history(self): def save_history(self):
logger.debug("Saving history") logger.debug("Saving history")
with open(os.path.join(self.data_dir, "chats", "chats.json"), "w+") as f: with open(os.path.join(self.data_dir, "chats", "chats.json"), "w+", encoding="utf-8") as f:
json.dump(self.chats, f, indent=4) json.dump(self.chats, f, indent=4)
def load_history_into_chat(self): def load_history_into_chat(self):
@@ -1116,9 +1207,9 @@ Generate a title following these rules:
if message: 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'))) 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': 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, id=key) 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: else:
self.show_message(message['content'], True, f"\n\n{message['model'].split(':')[0].replace('-', ' ').title()} ({message['model'].split(':')[1]})\n<small>{formated_date}</small>", id=key) 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.add_code_blocks()
self.bot_message = None self.bot_message = None
@@ -1126,14 +1217,23 @@ Generate a title following these rules:
logger.debug("Loading history") logger.debug("Loading history")
if os.path.exists(os.path.join(self.data_dir, "chats", "chats.json")): if os.path.exists(os.path.join(self.data_dir, "chats", "chats.json")):
try: try:
with open(os.path.join(self.data_dir, "chats", "chats.json"), "r") as f: with open(os.path.join(self.data_dir, "chats", "chats.json"), "r", encoding="utf-8") as f:
self.chats = json.load(f) self.chats = json.load(f)
if len(list(self.chats["chats"].keys())) == 0: self.chats["chats"][_("New Chat")] = {"messages": {}} if len(list(self.chats["chats"].keys())) == 0:
if "selected_chat" not in self.chats or self.chats["selected_chat"] not in self.chats["chats"]: self.chats["selected_chat"] = list(self.chats["chats"].keys())[0] self.chats["chats"][_("New Chat")] = {"messages": {}}
if "selected_chat" not in self.chats or self.chats["selected_chat"] not in self.chats["chats"]:
self.chats["selected_chat"] = list(self.chats["chats"].keys())[0]
if "order" not in self.chats: if "order" not in self.chats:
self.chats["order"] = [] self.chats["order"] = []
for chat_name in self.chats["chats"].keys(): for chat_name in self.chats["chats"].keys():
self.chats["order"].append(chat_name) self.chats["order"].append(chat_name)
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
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
self.chats = {"chats": {}, "selected_chat": None, "order": []} self.chats = {"chats": {}, "selected_chat": None, "order": []}
@@ -1182,7 +1282,8 @@ Generate a title following these rules:
def rename_chat(self, old_chat_name, new_chat_name, label_element): def rename_chat(self, old_chat_name, new_chat_name, label_element):
logger.info(f"Renaming chat \"{old_chat_name}\" -> \"{new_chat_name}\"") logger.info(f"Renaming chat \"{old_chat_name}\" -> \"{new_chat_name}\"")
new_chat_name = self.generate_numbered_name(new_chat_name, self.chats["chats"].keys()) new_chat_name = self.generate_numbered_name(new_chat_name, self.chats["chats"].keys())
if self.chats["selected_chat"] == old_chat_name: self.chats["selected_chat"] = new_chat_name if self.chats["selected_chat"] == old_chat_name:
self.chats["selected_chat"] = new_chat_name
self.chats["chats"][new_chat_name] = self.chats["chats"][old_chat_name] self.chats["chats"][new_chat_name] = self.chats["chats"][old_chat_name]
self.chats["order"][self.chats["order"].index(old_chat_name)] = new_chat_name self.chats["order"][self.chats["order"].index(old_chat_name)] = new_chat_name
del self.chats["chats"][old_chat_name] del self.chats["chats"][old_chat_name]
@@ -1252,9 +1353,12 @@ Generate a title following these rules:
gesture.connect("released", self.chat_click_handler) gesture.connect("released", self.chat_click_handler)
chat_row.add_controller(gesture) chat_row.add_controller(gesture)
if append: self.chat_list_box.append(chat_row) if append:
else: self.chat_list_box.prepend(chat_row) self.chat_list_box.append(chat_row)
if select: self.chat_list_box.select_row(chat_row) else:
self.chat_list_box.prepend(chat_row)
if select:
self.chat_list_box.select_row(chat_row)
def update_chat_list(self): def update_chat_list(self):
self.chat_list_box.remove_all() self.chat_list_box.remove_all()
@@ -1280,8 +1384,10 @@ Generate a title following these rules:
connection_handler.bearer_token = None connection_handler.bearer_token = None
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() local_instance.start()
if self.verify_connection() == False: self.connection_error() if self.verify_connection() == False:
else: self.remote_connection_switch.set_active(False) self.connection_error()
else:
self.remote_connection_switch.set_active(False)
def connection_error(self): def connection_error(self):
logger.error("Connection error") logger.error("Connection error")
@@ -1299,13 +1405,16 @@ Generate a title following these rules:
if self.run_remote: if self.run_remote:
connection_handler.bearer_token = self.remote_bearer_token connection_handler.bearer_token = self.remote_bearer_token
connection_handler.url = self.remote_url connection_handler.url = self.remote_url
if self.verify_connection() == False: self.connection_error() if self.verify_connection() == False:
else: local_instance.stop() self.connection_error()
else:
local_instance.stop()
else: else:
connection_handler.bearer_token = None connection_handler.bearer_token = None
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() local_instance.start()
if self.verify_connection() == False: self.connection_error() if self.verify_connection() == False:
self.connection_error()
def on_replace_contents(self, file, result): def on_replace_contents(self, file, result):
file.replace_contents_finish(result) file.replace_contents_finish(result)
@@ -1313,12 +1422,13 @@ Generate a title following these rules:
def on_export_chat(self, file_dialog, result, chat_name): def on_export_chat(self, file_dialog, result, chat_name):
file = file_dialog.save_finish(result) file = file_dialog.save_finish(result)
if not file: return if not file:
return
json_data = json.dumps({chat_name: self.chats["chats"][chat_name]}, indent=4).encode("UTF-8") json_data = json.dumps({chat_name: self.chats["chats"][chat_name]}, indent=4).encode("UTF-8")
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
json_path = os.path.join(temp_dir, "data.json") json_path = os.path.join(temp_dir, "data.json")
with open(json_path, "wb") as json_file: with open(json_path, "wb", encoding="utf-8") as json_file:
json_file.write(json_data) json_file.write(json_data)
tar_path = os.path.join(temp_dir, chat_name) tar_path = os.path.join(temp_dir, chat_name)
@@ -1328,7 +1438,7 @@ Generate a title following these rules:
if os.path.exists(directory) and os.path.isdir(directory): if os.path.exists(directory) and os.path.isdir(directory):
tar.add(directory, arcname=os.path.basename(directory)) tar.add(directory, arcname=os.path.basename(directory))
with open(tar_path, "rb") as tar: with open(tar_path, "rb", encoding="utf-8") as tar:
tar_content = tar.read() tar_content = tar.read()
file.replace_contents_async( file.replace_contents_async(
@@ -1347,7 +1457,8 @@ Generate a title following these rules:
def on_chat_imported(self, file_dialog, result): def on_chat_imported(self, file_dialog, result):
file = file_dialog.open_finish(result) file = file_dialog.open_finish(result)
if not file: return if not file:
return
stream = file.read(None) stream = file.read(None)
data_stream = Gio.DataInputStream.new(stream) data_stream = Gio.DataInputStream.new(stream)
tar_content = data_stream.read_bytes(1024 * 1024, None) tar_content = data_stream.read_bytes(1024 * 1024, None)
@@ -1355,7 +1466,7 @@ Generate a title following these rules:
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
tar_filename = os.path.join(temp_dir, "imported_chat.tar") tar_filename = os.path.join(temp_dir, "imported_chat.tar")
with open(tar_filename, "wb") as tar_file: with open(tar_filename, "wb", encoding="utf-8") as tar_file:
tar_file.write(tar_content.get_data()) tar_file.write(tar_content.get_data())
with tarfile.open(tar_filename, "r") as tar: with tarfile.open(tar_filename, "r") as tar:
@@ -1365,7 +1476,7 @@ Generate a title following these rules:
for member in tar.getmembers(): for member in tar.getmembers():
if member.name == "data.json": if member.name == "data.json":
json_filepath = os.path.join(temp_dir, member.name) json_filepath = os.path.join(temp_dir, member.name)
with open(json_filepath, "r") as json_file: with open(json_filepath, "r", encoding="utf-8") as json_file:
data = json.load(json_file) data = json.load(json_file)
for chat_name, chat_content in data.items(): for chat_name, chat_content in data.items():
new_chat_name = self.generate_numbered_name(chat_name, list(self.chats['chats'].keys())) new_chat_name = self.generate_numbered_name(chat_name, list(self.chats['chats'].keys()))
@@ -1413,11 +1524,12 @@ Generate a title following these rules:
logger.error(e) logger.error(e)
self.show_toast(_("Cannot open image"), self.main_overlay) self.show_toast(_("Cannot open image"), self.main_overlay)
elif file_type == 'plain_text' or file_type == 'youtube' or file_type == 'website': elif file_type == 'plain_text' or file_type == 'youtube' or file_type == 'website':
with open(file_path, 'r') as f: with open(file_path, 'r', encoding="utf-8") as f:
return f.read() return f.read()
elif file_type == 'pdf': elif file_type == 'pdf':
reader = PdfReader(file_path) reader = PdfReader(file_path)
if len(reader.pages) == 0: return None if len(reader.pages) == 0:
return None
text = "" text = ""
for i, page in enumerate(reader.pages): for i, page in enumerate(reader.pages):
text += f"\n- Page {i}\n{page.extract_text(extraction_mode='layout', layout_mode_space_vertically=False)}\n" text += f"\n- Page {i}\n{page.extract_text(extraction_mode='layout', layout_mode_space_vertically=False)}\n"
@@ -1428,7 +1540,8 @@ Generate a title following these rules:
button = self.attachments[name]['button'] button = self.attachments[name]['button']
button.get_parent().remove(button) button.get_parent().remove(button)
del self.attachments[name] del self.attachments[name]
if len(self.attachments) == 0: self.attachment_box.set_visible(False) if len(self.attachments) == 0:
self.attachment_box.set_visible(False)
def attach_file(self, file_path, file_type): def attach_file(self, file_path, file_type):
logger.debug(f"Attaching file: {file_path}") logger.debug(f"Attaching file: {file_path}")
@@ -1539,26 +1652,35 @@ Generate a title following these rules:
factory.connect("bind", self.on_model_dropdown_bind) factory.connect("bind", self.on_model_dropdown_bind)
self.model_drop_down.set_factory(factory) self.model_drop_down.set_factory(factory)
def handle_enter_key(self):
self.send_message()
return True
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
GtkSource.init() GtkSource.init()
with open('/app/share/Alpaca/alpaca/available_models.json', 'r') as f: with open(os.path.join(source_dir, 'available_models.json'), 'r', encoding="utf-8") as f:
self.available_models = json.load(f) self.available_models = json.load(f)
if not os.path.exists(os.path.join(self.data_dir, "chats")): if not os.path.exists(os.path.join(self.data_dir, "chats")):
os.makedirs(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)
self.message_text_view.add_controller(key_controller)
self.set_help_overlay(self.shortcut_window) self.set_help_overlay(self.shortcut_window)
self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash']) self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash'])
self.get_application().create_action('new_chat', lambda *_: self.new_chat(), ['<primary>n']) self.get_application().create_action('new_chat', lambda *_: self.new_chat(), ['<primary>n'])
self.get_application().create_action('clear', lambda *_: dialogs.clear_chat(self), ['<primary>e']) self.get_application().create_action('clear', lambda *_: dialogs.clear_chat(self), ['<primary>e'])
self.get_application().create_action('send', lambda *_: self.send_message(self), ['Return'])
self.get_application().create_action('import_chat', lambda *_: self.import_chat(), ['<primary>i']) self.get_application().create_action('import_chat', lambda *_: self.import_chat(), ['<primary>i'])
self.get_application().create_action('create_model_from_existing', lambda *_: dialogs.create_model_from_existing(self)) self.get_application().create_action('create_model_from_existing', lambda *_: dialogs.create_model_from_existing(self))
self.get_application().create_action('create_model_from_file', lambda *_: dialogs.create_model_from_file(self)) 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_chat', self.chat_actions)
self.get_application().create_action('rename_chat', self.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('rename_current_chat', self.current_chat_actions)
self.get_application().create_action('export_chat', self.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('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.message_text_view.connect("paste-clipboard", self.on_clipboard_paste) 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.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.add_chat_button.connect("clicked", lambda button : self.new_chat())
@@ -1569,7 +1691,7 @@ Generate a title following these rules:
self.background_switch.connect("notify", lambda pspec, user_data : self.switch_run_on_background()) self.background_switch.connect("notify", lambda pspec, user_data : self.switch_run_on_background())
self.setup_model_dropdown() self.setup_model_dropdown()
if os.path.exists(os.path.join(self.config_dir, "server.json")): if os.path.exists(os.path.join(self.config_dir, "server.json")):
with open(os.path.join(self.config_dir, "server.json"), "r") as f: with open(os.path.join(self.config_dir, "server.json"), "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
self.run_remote = data['run_remote'] self.run_remote = data['run_remote']
local_instance.port = data['local_port'] local_instance.port = data['local_port']
@@ -1582,7 +1704,8 @@ Generate a title following these rules:
self.seed_spin.set_value(self.model_tweaks['seed']) self.seed_spin.set_value(self.model_tweaks['seed'])
self.keep_alive_spin.set_value(self.model_tweaks['keep_alive']) self.keep_alive_spin.set_value(self.model_tweaks['keep_alive'])
#Overrides #Overrides
if "ollama_overrides" in data: local_instance.overrides = data['ollama_overrides'] if "ollama_overrides" in data:
local_instance.overrides = data['ollama_overrides']
for element in [ for element in [
self.override_HSA_OVERRIDE_GFX_VERSION, self.override_HSA_OVERRIDE_GFX_VERSION,
self.override_CUDA_VISIBLE_DEVICES, self.override_CUDA_VISIBLE_DEVICES,
@@ -1591,11 +1714,6 @@ Generate a title following these rules:
if override in local_instance.overrides: if override in local_instance.overrides:
element.set_text(local_instance.overrides[override]) element.set_text(local_instance.overrides[override])
#Support dialog
if 'show_support' not in data or data['show_support']:
if random.randint(0, 49) == 0:
dialogs.support(self)
if 'show_support' in data: self.show_support = data['show_support']
self.background_switch.set_active(self.run_on_background) self.background_switch.set_active(self.run_on_background)
self.set_hide_on_close(self.run_on_background) self.set_hide_on_close(self.run_on_background)
self.remote_connection_entry.set_text(self.remote_url) self.remote_connection_entry.set_text(self.remote_url)
@@ -1613,7 +1731,8 @@ Generate a title following these rules:
local_instance.start() 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) self.welcome_dialog.present(self)
if self.verify_connection() is False: self.connection_error() if self.verify_connection() is False:
self.connection_error()
self.update_list_available_models() self.update_list_available_models()
self.load_history() self.load_history()
self.update_chat_list() self.update_chat_list()

View File

@@ -5,7 +5,7 @@
<template class="AlpacaWindow" parent="AdwApplicationWindow"> <template class="AlpacaWindow" parent="AdwApplicationWindow">
<signal name="close-request" handler="closing_app"/> <signal name="close-request" handler="closing_app"/>
<property name="resizable">True</property> <property name="resizable">True</property>
<property name="width-request">360</property> <property name="width-request">400</property>
<property name="height-request">400</property> <property name="height-request">400</property>
<property name="default-width">1300</property> <property name="default-width">1300</property>
<property name="default-height">800</property> <property name="default-height">800</property>
@@ -14,21 +14,6 @@
<object class="AdwBreakpoint"> <object class="AdwBreakpoint">
<condition>max-width: 800sp</condition> <condition>max-width: 800sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter> <setter object="split_view_overlay" property="collapsed">true</setter>
<setter object="show_sidebar_button" property="visible">true</setter>
</object>
</child>
<child>
</child>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 500sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter>
<setter object="show_sidebar_button" property="visible">true</setter>
<setter object="welcome_dialog" property="width-request">360</setter>
<setter object="manage_models_dialog" property="width-request">360</setter>
<setter object="create_model_dialog" property="width-request">360</setter>
<setter object="preferences_dialog" property="width-request">360</setter>
<setter object="file_preview_dialog" property="width-request">360</setter>
</object> </object>
</child> </child>
<property name="content"> <property name="content">
@@ -80,7 +65,6 @@
<object class="AdwHeaderBar" id="header_bar"> <object class="AdwHeaderBar" id="header_bar">
<child type="start"> <child type="start">
<object class="GtkToggleButton" id="show_sidebar_button"> <object class="GtkToggleButton" id="show_sidebar_button">
<property name="visible">false</property>
<property name="icon-name">sidebar-show-symbolic</property> <property name="icon-name">sidebar-show-symbolic</property>
<property name="tooltip-text" translatable="yes">Toggle Sidebar</property> <property name="tooltip-text" translatable="yes">Toggle Sidebar</property>
<property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/> <property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
@@ -93,7 +77,7 @@
<child> <child>
<object class="GtkDropDown" id="model_drop_down"> <object class="GtkDropDown" id="model_drop_down">
<signal name="notify" handler="verify_if_image_can_be_used"/> <signal name="notify" handler="verify_if_image_can_be_used"/>
<property name="width-request">150</property> <property name="width-request">260</property>
<property name="enable-search">true</property> <property name="enable-search">true</property>
<property name="tooltip-text">Select Model</property> <property name="tooltip-text">Select Model</property>
<property name="model"> <property name="model">
@@ -104,7 +88,7 @@
</property> </property>
</object> </object>
</child> </child>
<child> <!--<child>
<object class="GtkButton" id="manage_models_button"> <object class="GtkButton" id="manage_models_button">
<signal name="clicked" handler="manage_models_button_activate"/> <signal name="clicked" handler="manage_models_button_activate"/>
<property name="tooltip-text" translatable="yes">Manage Models</property> <property name="tooltip-text" translatable="yes">Manage Models</property>
@@ -114,7 +98,7 @@
</object> </object>
</child> </child>
</object> </object>
</child> </child>-->
</object> </object>
</property> </property>
<child type="end"> <child type="end">
@@ -217,6 +201,8 @@
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="max-content-height">150</property> <property name="max-content-height">150</property>
<property name="propagate-natural-height">true</property> <property name="propagate-natural-height">true</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<style> <style>
<class name="message_input_scroll_window"/> <class name="message_input_scroll_window"/>
</style> </style>
@@ -226,8 +212,6 @@
<class name="message_text_view"/> <class name="message_text_view"/>
</style> </style>
<property name="wrap-mode">word</property> <property name="wrap-mode">word</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="top-margin">10</property> <property name="top-margin">10</property>
<property name="bottom-margin">10</property> <property name="bottom-margin">10</property>
<property name="hexpand">true</property> <property name="hexpand">true</property>
@@ -246,6 +230,7 @@
<style> <style>
<class name="accent"/> <class name="accent"/>
<class name="circular"/> <class name="circular"/>
<class name="suggested-action"/>
</style> </style>
<child> <child>
<object class="AdwButtonContent"> <object class="AdwButtonContent">
@@ -456,127 +441,6 @@
</child> </child>
</object> </object>
<object class="AdwDialog" id="create_model_dialog">
<property name="can-close">true</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToastOverlay" id="create_model_overlay">
<child>
<object class="AdwToolbarView">
<child type="bottom">
<object class="GtkActionBar">
<property name="revealed">true</property>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">Create</property>
<signal name="clicked" handler="create_model_start"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<child type="top">
<object class="AdwHeaderBar">
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">Create Model</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkBox">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="orientation">1</property>
<property name="spacing">12</property>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow" id="create_model_base">
<property name="title" translatable="yes">Base</property>
<property name="subtitle"/>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwEntryRow" id="create_model_name">
<property name="title" translatable="yes">Name</property>
<property name="input-purpose">alpha</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="create_model_system">
<property name="title" translatable="yes">Context</property>
<property name="input-purpose">alpha</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwEntryRow" id="create_model_template">
<property name="title" translatable="yes">Template</property>
<property name="input-purpose">alpha</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Some models require a specific template. Please visit the model's website for more information if you're unsure.</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="halign">1</property>
<property name="wrap">true</property>
<style>
<class name="caption"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<object class="AdwDialog" id="manage_models_dialog"> <object class="AdwDialog" id="manage_models_dialog">
<property name="can-close">true</property> <property name="can-close">true</property>
<property name="width-request">400</property> <property name="width-request">400</property>
@@ -730,6 +594,143 @@
</property> </property>
</object> </object>
</child> </child>
<child>
<object class="AdwNavigationPage">
<property name="title" translatable="yes">Create Model</property>
<property name="tag">model_create_page</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="icon-name">globe-symbolic</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkBox">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="orientation">1</property>
<property name="spacing">12</property>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow" id="create_model_base">
<property name="title" translatable="yes">Base</property>
<property name="sensitive">false</property>
<property name="subtitle"/>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwEntryRow" id="create_model_name">
<property name="title" translatable="yes">Name</property>
<property name="input-purpose">alpha</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="create_model_system">
<property name="title" translatable="yes">Context</property>
<property name="input-purpose">alpha</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="GtkBox">
<property name="height-request">140</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<style>
<class name="card"/>
</style>
<child>
<object class="GtkScrolledWindow">
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<child>
<object class="GtkTextView" id="create_model_modelfile">
<style>
<class name="modelfile_textview"/>
</style>
<property name="wrap-mode">word</property>
<property name="top-margin">10</property>
<property name="bottom-margin">10</property>
<property name="hexpand">true</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Some models require a modelfile, Alpaca fills FROM and SYSTEM (context) instructions automatically. Please visit the model's website or Ollama documentation for more information if you're unsure.</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="halign">1</property>
<property name="wrap">true</property>
<style>
<class name="caption"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Create</property>
<signal name="clicked" handler="create_model_start"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@@ -1032,6 +1033,10 @@ By downloading any model you accept their license agreement available on the mod
<attribute name="label" translatable="yes">Import Chat</attribute> <attribute name="label" translatable="yes">Import Chat</attribute>
<attribute name="action">app.import_chat</attribute> <attribute name="action">app.import_chat</attribute>
</item> </item>
<item>
<attribute name="label" translatable="yes">Manage Models</attribute>
<attribute name="action">app.manage_models</attribute>
</item>
</section> </section>
<section> <section>
<item> <item>
@@ -1085,9 +1090,13 @@ By downloading any model you accept their license agreement available on the mod
<attribute name="action">app.create_model_from_existing</attribute> <attribute name="action">app.create_model_from_existing</attribute>
</item> </item>
<item> <item>
<attribute name="label" translatable="yes">From GGUF File (Experimental)</attribute> <attribute name="label" translatable="yes">From GGUF File</attribute>
<attribute name="action">app.create_model_from_file</attribute> <attribute name="action">app.create_model_from_file</attribute>
</item> </item>
<item>
<attribute name="label" translatable="yes">From Name</attribute>
<attribute name="action">app.create_model_from_name</attribute>
</item>
</section> </section>
</menu> </menu>
<object class="GtkFileFilter" id="file_filter_attachments"> <object class="GtkFileFilter" id="file_filter_attachments">
@@ -1130,7 +1139,7 @@ By downloading any model you accept their license agreement available on the mod
<property name="title" translatable="yes">General</property> <property name="title" translatable="yes">General</property>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;ctrl&gt;Q</property> <property name="accelerator">&lt;ctrl&gt;W</property>
<property name="title" translatable="yes">Close application</property> <property name="title" translatable="yes">Close application</property>
</object> </object>
</child> </child>
@@ -1148,7 +1157,7 @@ By downloading any model you accept their license agreement available on the mod
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;ctrl&gt;P</property> <property name="accelerator">&lt;ctrl&gt;comma</property>
<property name="title" translatable="yes">Preferences</property> <property name="title" translatable="yes">Preferences</property>
</object> </object>
</child> </child>
@@ -1164,6 +1173,18 @@ By downloading any model you accept their license agreement available on the mod
<property name="title" translatable="yes">Show shortcuts window</property> <property name="title" translatable="yes">Show shortcuts window</property>
</object> </object>
</child> </child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;ctrl&gt;M</property>
<property name="title" translatable="yes">Manage models</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">F9</property>
<property name="title" translatable="yes">Toggle sidebar</property>
</object>
</child>
</object> </object>
</child> </child>
<child> <child>

View File

@@ -1,9 +1,15 @@
"""
Moves the descriptions of models to src/available_models_descriptions.py
so they can be translated
"""
import json import json
with open('src/available_models.json', 'r') as f:
if __name__ == "__main__":
with open('src/available_models.json', 'r', encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
results = 'descriptions = {\n' RESULTS = 'descriptions = {\n'
for key, value in data.items(): for key, value in data.items():
results += f" '{key}': _(\"{value['description']}\"),\n" RESULTS += f" '{key}': _(\"{value['description']}\"),\n"
results += '}' RESULTS += '}'
with open('src/available_models_descriptions.py', 'w+') as f: with open('src/available_models_descriptions.py', 'w+', encoding="utf-8") as f:
f.write(results) f.write(RESULTS)