Major update
- Added settings - Added welcome dialog - Added custom system message (kinda #4) - Added markdown in messages - Added streamed responses - Completely reworked UI concept - Multi chat support - Added conversation titles - Fixed #3 (visually the same)
48
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
A modern and easy-to-use client for Ollama. Have the greatest experience while keeping everything private and in your local network.
|
||||
|
||||
|  |  |  |  |
|
||||
|  |  |  |  |
|
||||
|-|-|-|-|
|
||||
|
||||
> Important: This app does not host a Ollama server on device, but rather connects to one and uses its api endpoint.
|
||||
|
@ -26,28 +26,37 @@ You'll find the latest recommended version of the Ollama App under [the releases
|
|||
|
||||
After installing the app and opening it for the first time, you'll encounter this popup:
|
||||
|
||||

|
||||

|
||||
|
||||
In this dialog, you have to enter the base URL of your instance. The port is required, except for the port number matching the protocol (443 for HTTPS or 80 for HTTP).
|
||||
Go through the welcome dialog one by one, you should read their content, but you don't have to.
|
||||
|
||||
This address will be checked, so no worry about entering the wrong one. The disadvantage of this is that your server has to be running even if you don't want to chat with it at that moment. The checkup only happens on initial setup for now. If you move your server or the server goes down and you try to send a message to it, there is a chance of the app crashing. Don't worry, just go into the side menu and click the settings button to change it.
|
||||
|  |  |  |
|
||||
|-|-|-|
|
||||
|
||||
After going through that, you'll get a small snack bar notifying you that you have to set the host. For that, open the sidebar (swipe from the left to right or click the icon in the top left corner) and click on settings. There you'll find all app-related settings, you should go through them, but for the initial setup, only the first one is important.
|
||||
|
||||
In the bit host text field, you have to enter the base URL of your instance. The port is required, except for the port number matching the protocol (443 for HTTPS or 80 for HTTP). After that, click the save icon right next to the text field.
|
||||
|
||||
This address will be checked, so no worry about entering the wrong one. The disadvantage of this is that your server has to be running even if you don't want to chat with it at that moment. If you set the host once, and your server is offline, the requests will fail, but the host will stay saved if you don't change it yourself. Don't worry, just go into the side menu and click the settings button to change it.
|
||||
|
||||
That's it, you can now just chat. Enter a message into the box at the bottom and click the send icon.
|
||||
|
||||
## Side Menu
|
||||
|
||||
The button on the top left opens the menu. In it, you have three options: `New Chat`, `Ask before Deletion` and `Set Host`. The first option clears the chat (-> creates a new one), the second one opens a new dialog that has a toggle you can toggle if you don't want to be asked or want to be asked again before deleting a chat, and the third option reopens the host dialog from the initial start of the app to adapt to changing hosts.
|
||||
The button on the top left opens the menu. In it, you have two options: `New Chat` and `Settings`. The first option creates a new chat, and the second one opens the settings screen where you can change how everything works.
|
||||
|
||||

|
||||
Below that are all the chats. To delete one, swipe it from left to right. To rename the chat tab and hold it until a popup dialog appears. In it, you can change the title or tab the sparkle icon to let AI find one for you. This is not affected by the "generate title" setting.
|
||||
|
||||
Note: The same effect as the `New Chat` option has the button on the top right of the main screen.
|
||||
| |  |
|
||||
|-|-|
|
||||
|
||||
> Note: The button on the top right corner deletes the chat. It has the same effect as swiping the chat in the sidebar.
|
||||
|
||||
## Model Selector
|
||||
|
||||
You can access the model selector by tapping on the `<selector>` text in the top middle or the name of the currently selected model in the same spot. Then you'll get the following bottom sheet:
|
||||
|
||||

|
||||

|
||||
|
||||
This will display all the models currently installed in your Ollama server instance.
|
||||
|
||||
|
@ -55,17 +64,18 @@ Models with an image-like icon next to them allow multimodal input. The one show
|
|||
|
||||
The models with a star next to them are recommended models. They have been selected by me (hehe) to be listed as that. Read more under [Custom Builds](#custom-builds).
|
||||
|
||||
The `Add Model` button does nothing at the moment, it just opens a dialog that lists steps on how to add a model to an instance. For safety reasons, I didn't add the ability to add a model directly via name in the app.
|
||||
The `Add` button does nothing at the moment, it just opens a snack bar listing steps on how to add a model to an instance. For safety reasons, I didn't add the ability to add a model directly via name in the app.
|
||||
|
||||
## Multimodal Model Input
|
||||
|
||||
Ollama App supports multimodal models, models with support input via an image.
|
||||
|
||||
After selecting one in the model selector, a new icon appears at the bottom left of the message bar; a camera icon. Clicking on it reveals the following bottom sheet:
|
||||
After selecting a supported model, as describes in [Model Selector](#model-selector), a new icon appears at the bottom left of the message bar; a camera icon. Clicking on it reveals the following bottom sheet:
|
||||
|
||||

|
||||
|  |  |
|
||||
|-|-|
|
||||
|
||||
Select one of them, take your photo and it'll get added to the chat. You can also add multiple.
|
||||
Select one of them, take or select your photo and it'll get added to the chat. You can also add multiple.
|
||||
|
||||
Even though the images will appear in the chat already after sending, they won't be submitted to the AI until a new text message is sent.
|
||||
|
||||
|
@ -85,19 +95,27 @@ Now comes the interesting part. I built this app in a way you can easily create
|
|||
```
|
||||
// use host or not, if false dialog is shown
|
||||
const useHost = false;
|
||||
// host of ollama, must be accessible from the client, without trailing slash
|
||||
const fixedHost = "http://example.com:1144";
|
||||
// host of ollama, must be accessible from the client, without trailing slash, will always be accepted as valid
|
||||
const fixedHost = "http://example.com:11434";
|
||||
// use model or not, if false selector is shown
|
||||
const useModel = false;
|
||||
// model name as string, must be valid ollama model!
|
||||
const fixedModel = "gemma";
|
||||
// recommended models, shown with as star in model selector
|
||||
const recommendedModels = ["gemma", "llama3"];
|
||||
// allow opening of settings
|
||||
const allowSettings = true;
|
||||
// allow multiple chats
|
||||
const allowMultipleChats = true;
|
||||
```
|
||||
|
||||
They can be found at the top of `lib/main.dart`. `useHost` and `useModel` decide whether you want `fixedHost` and `fixedModel` to control anything. `fixedHost` and `fixedModel` decide about the value that has to be used. That can be practical in case you try to create an app specific to your instance.
|
||||
|
||||
The last one, `recommendedModels`, is a list of models that will be listed as recommended in the [Model Selector](#model-selector). They are more like personal preferences. If empty, no model will be preferred.
|
||||
`recommendedModels` is a list of models that will be listed as recommended in the [Model Selector](#model-selector). They are more like personal preferences. If empty, no model will be preferred.
|
||||
|
||||
`allowSettings` will disable the settings screen. But it will also disable the welcome dialog at first startup and the ability to rename chats.
|
||||
|
||||
`allowMultipleChats` simply removes the `New Chat` option in the [Side Menu](#side-menu). And will load up the only available chat on app startup.
|
||||
|
||||
### Actually Building
|
||||
|
||||
|
|
|
@ -16,12 +16,12 @@
|
|||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
|
@ -37,9 +37,19 @@
|
|||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
<!-- check if url is valid -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<!-- check if url is valid end -->
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 152 KiB |
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 120 KiB |
|
@ -0,0 +1,9 @@
|
|||
- Einstellungen hinzugefügt
|
||||
- Begrüßungsdialog hinzugefügt
|
||||
- Hinzugefügte benutzerdefinierte Systemnachricht
|
||||
- Markdown in Nachrichten hinzugefügt
|
||||
- Gestreamte Antworten hinzugefügt
|
||||
- Vollständig überarbeitetes UI-Konzept
|
||||
- Unterstützung für Mehrfachchats
|
||||
- Konversationstitel hinzugefügt
|
||||
- Fehler behoben, wenn die Modellversion nicht `latest` ist
|
|
@ -0,0 +1,9 @@
|
|||
- Added settings
|
||||
- Added welcome dialog
|
||||
- Added custom system message
|
||||
- Added markdown in messages
|
||||
- Added streamed responses
|
||||
- Completely reworked UI concept
|
||||
- Multi chat support
|
||||
- Added conversation titles
|
||||
- Fixed bug when model version is not `latest`
|
|
@ -10,16 +10,46 @@
|
|||
"description": "Text displayed for new chat option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionSetHost": "Host Setzen",
|
||||
"@optionSetHost": {
|
||||
"description": "Text displayed for set host option",
|
||||
"optionSettings": "Einstellungen",
|
||||
"@optionSettings": {
|
||||
"description": "Text displayed for settings option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionSetAskDeletion": "Vor dem Löschen fragen",
|
||||
"@optionSetAskDeletion": {
|
||||
"description": "Text displayed for ask before deletion option",
|
||||
"optionNoChatFound": "Keine Chats gefunden",
|
||||
"@optionNoChatFound": {
|
||||
"description": "Text displayed when no chats are found",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"tipPrefix": "Tipp: ",
|
||||
"@tipPrefix": {
|
||||
"description": "Prefix for tips",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip0": "Bearbeite Nachrichten durch langes Tippen",
|
||||
"@tip0": {
|
||||
"description": "First tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip1": "Löschen Sie Nachrichten durch Doppeltippen",
|
||||
"@tip1": {
|
||||
"description": "Second tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip2": "Das Thema kann in den Einstellungen geändert werden",
|
||||
"@tip2": {
|
||||
"description": "Third tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip3": "Wähle ein multimodales Modell zum Anhängen von Bildern",
|
||||
"@tip3": {
|
||||
"description": "Fourth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip4": "Chats werden automatisch gespeichert",
|
||||
"@tip4": {
|
||||
"description": "Fifth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"takeImage": "Bild Aufnehmen",
|
||||
"@takeImage": {
|
||||
"description": "Text displayed for take image button",
|
||||
|
@ -30,6 +60,11 @@
|
|||
"description": "Text displayed for image upload button",
|
||||
"context": "Visible in attachment menu"
|
||||
},
|
||||
"notAValidImage": "Kein gültiges Bild",
|
||||
"@notAValidImage": {
|
||||
"description": "Text displayed when an image is not valid",
|
||||
"context": "Visible in the chat view"
|
||||
},
|
||||
"messageInputPlaceholder": "Nachricht",
|
||||
"@messageInputPlaceholder": {
|
||||
"description": "Placeholder text for message input",
|
||||
|
@ -40,57 +75,37 @@
|
|||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view"
|
||||
},
|
||||
"hostDialogTitle": "Host Setzen",
|
||||
"@hostDialogTitle": {
|
||||
"description": "Title of the host dialog",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogDescription": "Gebe den Host des Ollama-Servers ein. Dies wird validiert und kann später in den Einstellungen geändert werden.",
|
||||
"@hostDialogDescription": {
|
||||
"description": "Description of the host dialog",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogErrorInvalidHost": "Der Host konnte nicht validiert werden, bitte versuche es erneut. Entweder ist er nicht erreichbar oder es handelt sich nicht um eine gültige Ollama-Serverinstanz.",
|
||||
"@hostDialogErrorInvalidHost": {
|
||||
"description": "Error message displayed when the host is invalid",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogErrorInvalidUrl": "Die URL ist ungültig. Versuche, sie erneut zu überprüfen.",
|
||||
"@hostDialogErrorInvalidUrl": {
|
||||
"description": "Error message displayed when the URL is invalid",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogSave": "Host Speichern",
|
||||
"@hostDialogSave": {
|
||||
"description": "Text displayed for save host button, should be capitalized",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogCancel": "Abbrechen",
|
||||
"@hostDialogCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the host dialog"
|
||||
"noHostSelected": "Kein Host ausgewählt, öffne zum Auswählen die Einstellungen",
|
||||
"@noSelectedHost": {
|
||||
"description": "Text displayed when no host is selected",
|
||||
"context": "Visible in the chat view, opens the settings dialog when clicked"
|
||||
},
|
||||
"noSelectedModel": "<selektor>",
|
||||
"@noSelectedModel": {
|
||||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view, opens the model dialog when clicked"
|
||||
},
|
||||
"modelDialogAddModel": "Modell hinzufügen",
|
||||
"newChatTitle": "Unbenannter Chat",
|
||||
"@newChatTitle": {
|
||||
"description": "Title of a new chat",
|
||||
"context": "Visible in the new chat dialog"
|
||||
},
|
||||
"modelDialogAddModel": "Hinzufügen",
|
||||
"@modelDialogAddModel": {
|
||||
"description": "Text displayed for add model button",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddSteps": "Um ein neues Modell hinzuzufügen, besuche ollama.com auf deinem Computer, suche nach einem Modell, das dir gefällt, kopieren den Befehl und füge ihn in ein neues Terminalfenster ein. Warten, bis der Download abgeschlossen ist, dies kann eine Weile dauern. Sobald dieser abgeschlossen ist, öffne diesen Selektor erneut und finden dein neu hinzugefügtes Modell.",
|
||||
"modelDialogAddSteps": "Das Hinzufügen von Modellen wird nicht unterstützt. Gehe zu deinem Host-PC und füge dort Modelle hinzu.",
|
||||
"@modelDialogAddSteps": {
|
||||
"description": "Steps to add a new model",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"deleteDialogTitle": "Löschen Bestätigen",
|
||||
"deleteDialogTitle": "Chat löschen",
|
||||
"@deleteDialogTitle": {
|
||||
"description": "Title of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
},
|
||||
"deleteDialogDescription": "Bist du sicher, dass du fortfahren möchtest? Dies wird alle Erinnerungen dieses Chats löschen und kann nicht rückgängig gemacht werden.",
|
||||
"deleteDialogDescription": "Bist du sicher, dass du fortfahren möchtest? Dies wird alle Erinnerungen dieses Chats löschen und kann nicht rückgängig gemacht werden.\nUm diesen Dialog zu deaktivieren, besuche die Einstellungen.",
|
||||
"@deleteDialogDescription": {
|
||||
"description": "Description of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
|
@ -105,9 +120,156 @@
|
|||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the delete dialog"
|
||||
},
|
||||
"deleteDialogAskAlways": "Jedesmal nachfragen",
|
||||
"@deleteDialogAskAlways": {
|
||||
"description": "Text displayed for ask me always again checkbox",
|
||||
"context": "Visible in the delete dialog"
|
||||
"dialogEnterNewTitle": "Gib bitte einen neuen Titel ein",
|
||||
"@dialogEnterNewTitle": {
|
||||
"description": "Text displayed as description for new title input",
|
||||
"context": "Visible in the rename dialog"
|
||||
},
|
||||
"dialogEditMessageTitle": "Nachricht bearbeiten",
|
||||
"@dialogEditMessageTitle": {
|
||||
"description": "Title of the edit message dialog",
|
||||
"context": "Visible in the edit message dialog"
|
||||
},
|
||||
"settingsTitleBehavior": "Verhalten",
|
||||
"@settingsTitleBehavior": {
|
||||
"description": "Title of the behavior settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleInterface": "Oberfläche",
|
||||
"@settingsTitleInterface": {
|
||||
"description": "Title of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleContact": "Kontakt",
|
||||
"@settingsTitleContact": {
|
||||
"description": "Title of the contact settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHost": "Host",
|
||||
"@settingsHost": {
|
||||
"description": "Text displayed as description for host input",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostValid": "Gültiger Host",
|
||||
"@settingsHostValid": {
|
||||
"description": "Text displayed when the host is valid",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostChecking": "Host wird Überprüft",
|
||||
"@settingsHostChecking": {
|
||||
"description": "Text displayed when the host is being checked",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostInvalid": "Fehler: {type, select, url{Ungültige URL} host{Ungültiger Host} other{Request Fehlgeschlagen}}",
|
||||
"@settingsHostInvalid": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsHostInvalidDetailed": "{type, select, url{Die eingegebene URL ist ungültig. Es handelt sich nicht um ein standardisiertes URL-Format.} other{Der eingegebene Host ist ungültig. Er kann nicht erreicht werden. Bitte überprüfe den Host und versuche es erneut.}}",
|
||||
"@settingsHostInvalidDetailed": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsSystemMessage": "Systemnachricht",
|
||||
"@settingsSystemMessage": {
|
||||
"description": "Text displayed as description for system message input",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsDisableMarkdown": "Markdown deaktivieren",
|
||||
"@settingsDisableMarkdown": {
|
||||
"description": "Text displayed as description for disable markdown toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBehaviorNotUpdatedForOlderChats": "Verhaltenseinstellungen werden nicht für ältere Chats aktualisiert. Starte einen neuen, um die Änderungen anzuwenden.",
|
||||
"@settingsBehaviorNotUpdatedForOlderChats": {
|
||||
"description": "Text displayed when behavior settings are not updated for older chats",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsGenerateTitles": "Titel generieren",
|
||||
"@settingsGenerateTitles": {
|
||||
"description": "Text displayed as description for generate titles toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsAskBeforeDelete": "Vor Löschung des Chats fragen",
|
||||
"@settingsAskBeforeDelete": {
|
||||
"description": "Text displayed as description for ask before deletion toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsResetOnModelChange": "Zurücksetzen bei Modelländerung",
|
||||
"@settingsResetOnModelChange": {
|
||||
"description": "Text displayed as description for reset on model change toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsEnableEditing": "Nachrichtenbearbeitung aktivieren",
|
||||
"@settingsEnableEditing": {
|
||||
"description": "Text displayed as description for enable editing toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsShowTips": "Tipps in der Seitenleiste anzeigen",
|
||||
"@settingsShowTips": {
|
||||
"description": "Text displayed as description for show tips toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessSystem": "System",
|
||||
"@settingsBrightnessSystem": {
|
||||
"description": "Text displayed as description for system brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessLight": "Hell",
|
||||
"@settingsBrightnessLight": {
|
||||
"description": "Text displayed as description for light brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessDark": "Dunkel",
|
||||
"@settingsBrightnessDark": {
|
||||
"description": "Text displayed as description for dark brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartTitle": "Neustart Erforderlich",
|
||||
"@settingsBrightnessRestartTitle": {
|
||||
"description": "Title of the restart required dialog",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartDescription": "Das Ändern des Themas erfordert einen Neustart.\nMöchtest du jetzt neu starten oder die Aktion abbrechen?",
|
||||
"@settingsBrightnessRestartDescription": {
|
||||
"description": "Description of the restart required dialog",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartRestart": "Neustarten",
|
||||
"@settingsBrightnessRestartRestart": {
|
||||
"description": "Text displayed for restart button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartCancel": "Abbrechen",
|
||||
"@settingsBrightnessRestartCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsGithub": "GitHub",
|
||||
"@settingsGithub": {
|
||||
"description": "Text displayed as description for GitHub button",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsReportIssue": "Einen Fehler melden",
|
||||
"@settingsReportIssue": {
|
||||
"description": "Text displayed as description for report issue button",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsMainDeveloper": "Hauptentwickler",
|
||||
"@settingsMainDeveloper": {
|
||||
"description": "Text displayed as description for main developer button",
|
||||
"context": "Visible in the settings view"
|
||||
}
|
||||
}
|
|
@ -10,16 +10,46 @@
|
|||
"description": "Text displayed for new chat option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionSetHost": "Set Host",
|
||||
"@optionSetHost": {
|
||||
"description": "Text displayed for set host option",
|
||||
"optionSettings": "Settings",
|
||||
"@optionSettings": {
|
||||
"description": "Text displayed for settings option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionSetAskDeletion": "Ask before Deletion",
|
||||
"@optionSetAskDeletion": {
|
||||
"description": "Text displayed for ask before deletion option",
|
||||
"optionNoChatFound": "No chats found",
|
||||
"@optionNoChatFound": {
|
||||
"description": "Text displayed when no chats are found",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"tipPrefix": "Tip: ",
|
||||
"@tipPrefix": {
|
||||
"description": "Prefix for tips",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip0": "Edit messages by long taping on them",
|
||||
"@tip0": {
|
||||
"description": "First tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip1": "Delete messages by double tapping on them",
|
||||
"@tip1": {
|
||||
"description": "Second tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip2": "You can change the theme in settings",
|
||||
"@tip2": {
|
||||
"description": "Third tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip3": "Select a multimodal model to input images",
|
||||
"@tip3": {
|
||||
"description": "Fourth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"tip4": "Chats are automatically saved",
|
||||
"@tip4": {
|
||||
"description": "Fifth tip displayed in the sidebar",
|
||||
"context": "Visible in the sidebar"
|
||||
},
|
||||
"takeImage": "Take Image",
|
||||
"@takeImage": {
|
||||
"description": "Text displayed for take image button",
|
||||
|
@ -30,6 +60,11 @@
|
|||
"description": "Text displayed for image upload button",
|
||||
"context": "Visible in attachment menu"
|
||||
},
|
||||
"notAValidImage": "Not a valid image",
|
||||
"@notAValidImage": {
|
||||
"description": "Text displayed when an image is not valid",
|
||||
"context": "Visible in the chat view"
|
||||
},
|
||||
"messageInputPlaceholder": "Message",
|
||||
"@messageInputPlaceholder": {
|
||||
"description": "Placeholder text for message input",
|
||||
|
@ -40,57 +75,37 @@
|
|||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view"
|
||||
},
|
||||
"hostDialogTitle": "Set Host",
|
||||
"@hostDialogTitle": {
|
||||
"description": "Title of the host dialog",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogDescription": "Enter the host of the Ollama server. This will be validated and can be changed in settings later.",
|
||||
"@hostDialogDescription": {
|
||||
"description": "Description of the host dialog",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogErrorInvalidHost": "The host could not be validated, please try again. Either it is not reachable or is not a valid Ollama server instance.",
|
||||
"@hostDialogErrorInvalidHost": {
|
||||
"description": "Error message displayed when the host is invalid",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogErrorInvalidUrl": "The URL is not valid. Try rechecking it.",
|
||||
"@hostDialogErrorInvalidUrl": {
|
||||
"description": "Error message displayed when the URL is invalid",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogSave": "Save Host",
|
||||
"@hostDialogSave": {
|
||||
"description": "Text displayed for save host button, should be capitalized",
|
||||
"context": "Visible in the host dialog"
|
||||
},
|
||||
"hostDialogCancel": "Cancel",
|
||||
"@hostDialogCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the host dialog"
|
||||
"noHostSelected": "No host selected, open setting to set one",
|
||||
"@noSelectedHost": {
|
||||
"description": "Text displayed when no host is selected",
|
||||
"context": "Visible in the chat view, opens the settings dialog when clicked"
|
||||
},
|
||||
"noSelectedModel": "<selector>",
|
||||
"@noSelectedModel": {
|
||||
"description": "Text displayed when no model is selected",
|
||||
"context": "Visible in the chat view, opens the model dialog when clicked"
|
||||
},
|
||||
"modelDialogAddModel": "Add Model",
|
||||
"newChatTitle": "Unnamed Chat",
|
||||
"@newChatTitle": {
|
||||
"description": "Title of a new chat",
|
||||
"context": "Visible in the new chat dialog"
|
||||
},
|
||||
"modelDialogAddModel": "Add",
|
||||
"@modelDialogAddModel": {
|
||||
"description": "Text displayed for add model button",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddSteps": "To add a new model, visit ollama.com on your computer, look for a model you like, copy the command and paste it in a new terminal window. Wait for the download to complete, this can take a while. Once it is complete, reopen this selector and you'll find your newly added model.",
|
||||
"modelDialogAddSteps": "Adding models is not supported. Go to your host pc and add models there.",
|
||||
"@modelDialogAddSteps": {
|
||||
"description": "Steps to add a new model",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"deleteDialogTitle": "Confirm Deletion",
|
||||
"deleteDialogTitle": "Delete Chat",
|
||||
"@deleteDialogTitle": {
|
||||
"description": "Title of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
},
|
||||
"deleteDialogDescription": "Are you sure you want to continue? This will wipe all memory of this chat and cannot be undone.",
|
||||
"deleteDialogDescription": "Are you sure you want to continue? This will wipe all memory of this chat and cannot be undone.\nTo disable this dialog, visit the settings.",
|
||||
"@deleteDialogDescription": {
|
||||
"description": "Description of the delete dialog",
|
||||
"context": "Visible in the delete dialog"
|
||||
|
@ -105,9 +120,156 @@
|
|||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the delete dialog"
|
||||
},
|
||||
"deleteDialogAskAlways": "Ask me every time",
|
||||
"@deleteDialogAskAlways": {
|
||||
"description": "Text displayed for ask me always again checkbox",
|
||||
"context": "Visible in the delete dialog"
|
||||
"dialogEnterNewTitle": "Enter new title",
|
||||
"@dialogEnterNewTitle": {
|
||||
"description": "Text displayed as description for new title input",
|
||||
"context": "Visible in the rename dialog"
|
||||
},
|
||||
"dialogEditMessageTitle": "Edit Message",
|
||||
"@dialogEditMessageTitle": {
|
||||
"description": "Title of the edit message dialog",
|
||||
"context": "Visible in the edit message dialog"
|
||||
},
|
||||
"settingsTitleBehavior": "Behavior",
|
||||
"@settingsTitleBehavior": {
|
||||
"description": "Title of the behavior settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleInterface": "Interface",
|
||||
"@settingsTitleInterface": {
|
||||
"description": "Title of the interface settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsTitleContact": "Contact",
|
||||
"@settingsTitleContact": {
|
||||
"description": "Title of the contact settings section",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHost": "Host",
|
||||
"@settingsHost": {
|
||||
"description": "Text displayed as description for host input",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostValid": "Valid Host",
|
||||
"@settingsHostValid": {
|
||||
"description": "Text displayed when the host is valid",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostChecking": "Checking Host",
|
||||
"@settingsHostChecking": {
|
||||
"description": "Text displayed when the host is being checked",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostInvalid": "Issue: {type, select, url{Invalid URL} host{Invalid Host} other{Request Failed}}",
|
||||
"@settingsHostInvalid": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsHostInvalidDetailed": "{type, select, url{The URL you entered is invalid. It isn't an a standardized URL format.} other{The host you entered is invalid. It cannot be reached. Please check the host and try again.}}",
|
||||
"@settingsHostInvalidDetailed": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsSystemMessage": "System message",
|
||||
"@settingsSystemMessage": {
|
||||
"description": "Text displayed as description for system message input",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsDisableMarkdown": "Disable markdown",
|
||||
"@settingsDisableMarkdown": {
|
||||
"description": "Text displayed as description for disable markdown toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBehaviorNotUpdatedForOlderChats": "Behavior settings are not updated for older chats. Start a new one to apply the changes.",
|
||||
"@settingsBehaviorNotUpdatedForOlderChats": {
|
||||
"description": "Text displayed when behavior settings are not updated for older chats",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsGenerateTitles": "Generate titles",
|
||||
"@settingsGenerateTitles": {
|
||||
"description": "Text displayed as description for generate titles toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsAskBeforeDelete": "Ask before chat deletion",
|
||||
"@settingsAskBeforeDelete": {
|
||||
"description": "Text displayed as description for ask before deletion toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsResetOnModelChange": "Reset on model change",
|
||||
"@settingsResetOnModelChange": {
|
||||
"description": "Text displayed as description for reset on model change toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsEnableEditing": "Enable editing of messages",
|
||||
"@settingsEnableEditing": {
|
||||
"description": "Text displayed as description for enable editing toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsShowTips": "Show tips in sidebar",
|
||||
"@settingsShowTips": {
|
||||
"description": "Text displayed as description for show tips toggle",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessSystem": "System",
|
||||
"@settingsBrightnessSystem": {
|
||||
"description": "Text displayed as description for system brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessLight": "Light",
|
||||
"@settingsBrightnessLight": {
|
||||
"description": "Text displayed as description for light brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessDark": "Dark",
|
||||
"@settingsBrightnessDark": {
|
||||
"description": "Text displayed as description for dark brightness option",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartTitle": "Restart Required",
|
||||
"@settingsBrightnessRestartTitle": {
|
||||
"description": "Title of the restart required dialog",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartDescription": "Changing the theme requires a restart.\nDo you want to restart now or cancel the action?",
|
||||
"@settingsBrightnessRestartDescription": {
|
||||
"description": "Description of the restart required dialog",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartRestart": "Restart",
|
||||
"@settingsBrightnessRestartRestart": {
|
||||
"description": "Text displayed for restart button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsBrightnessRestartCancel": "Cancel",
|
||||
"@settingsBrightnessRestartCancel": {
|
||||
"description": "Text displayed for cancel button, should be capitalized",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsGithub": "GitHub",
|
||||
"@settingsGithub": {
|
||||
"description": "Text displayed as description for GitHub button",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsReportIssue": "Report Issue",
|
||||
"@settingsReportIssue": {
|
||||
"description": "Text displayed as description for report issue button",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsMainDeveloper": "Main Developer",
|
||||
"@settingsMainDeveloper": {
|
||||
"description": "Text displayed as description for main developer button",
|
||||
"context": "Visible in the settings view"
|
||||
}
|
||||
}
|
890
lib/main.dart
|
@ -0,0 +1,446 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'main.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:simple_icons/simple_icons.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
|
||||
class ScreenSettings extends StatefulWidget {
|
||||
const ScreenSettings({super.key});
|
||||
|
||||
@override
|
||||
State<ScreenSettings> createState() => _ScreenSettingsState();
|
||||
}
|
||||
|
||||
class _ScreenSettingsState extends State<ScreenSettings> {
|
||||
final hostInputController = TextEditingController(
|
||||
text: (useHost) ? fixedHost : (prefs?.getString("host") ?? ""));
|
||||
bool hostLoading = false;
|
||||
bool hostInvalidUrl = false;
|
||||
bool hostInvalidHost = false;
|
||||
void checkHost() async {
|
||||
setState(() {
|
||||
hostLoading = true;
|
||||
hostInvalidUrl = false;
|
||||
hostInvalidHost = false;
|
||||
});
|
||||
var tmpHost = hostInputController.text.trim().removeSuffix("/").trim();
|
||||
|
||||
if (tmpHost.isEmpty || !Uri.parse(tmpHost).isAbsolute) {
|
||||
setState(() {
|
||||
hostInvalidUrl = true;
|
||||
hostLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
http.Response request;
|
||||
try {
|
||||
request = await http
|
||||
.get(Uri.parse(tmpHost))
|
||||
.timeout(const Duration(seconds: 5), onTimeout: () {
|
||||
return http.Response("Error", 408);
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
hostInvalidHost = true;
|
||||
hostLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ((request.statusCode == 200 && request.body == "Ollama is running") ||
|
||||
(Uri.parse(tmpHost).toString() == fixedHost)) {
|
||||
// messages = [];
|
||||
setState(() {
|
||||
hostLoading = false;
|
||||
host = tmpHost;
|
||||
if (hostInputController.text != host!) {
|
||||
hostInputController.text = host!;
|
||||
}
|
||||
});
|
||||
prefs?.setString("host", host!);
|
||||
} else {
|
||||
setState(() {
|
||||
hostInvalidHost = true;
|
||||
hostLoading = false;
|
||||
});
|
||||
}
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
final systemInputController = TextEditingController(
|
||||
text: prefs?.getString("system") ?? "You are a helpful assistant");
|
||||
|
||||
final repoUrl = "https://github.com/JHubi1/ollama-app";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
checkHost();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
hostInputController.dispose();
|
||||
}
|
||||
|
||||
Widget toggle(String text, bool value, Function(bool value) onChanged) {
|
||||
var space = ""; // Invisible character: U+2063
|
||||
var spacePlus = " $space";
|
||||
return Stack(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 12),
|
||||
child: Divider(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]
|
||||
: Colors.grey[900])),
|
||||
Row(mainAxisSize: MainAxisSize.max, children: [
|
||||
Expanded(
|
||||
child: Text(text + spacePlus,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
backgroundColor:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.surface))),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
child: SizedBox(
|
||||
height: 40, child: Switch(value: value, onChanged: onChanged)))
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget title(String text, {double top = 16, double bottom = 16}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, top: top, bottom: bottom),
|
||||
child: Row(children: [
|
||||
const Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Text(text)),
|
||||
const Expanded(child: Divider())
|
||||
]));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: !hostLoading,
|
||||
onPopInvoked: (didPop) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.optionSettings),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: ListView(children: [
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: hostInputController,
|
||||
keyboardType: TextInputType.url,
|
||||
readOnly: useHost,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.settingsHost,
|
||||
hintText: "http://localhost:11434",
|
||||
suffixIcon: useHost
|
||||
? const SizedBox.shrink()
|
||||
: (hostLoading
|
||||
? Transform.scale(
|
||||
scale: 0.5,
|
||||
child: const CircularProgressIndicator())
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
checkHost();
|
||||
},
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
)),
|
||||
border: const OutlineInputBorder(),
|
||||
error: (hostInvalidHost || hostInvalidUrl)
|
||||
? InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsHostInvalidDetailed(
|
||||
hostInvalidHost
|
||||
? "host"
|
||||
: "url")),
|
||||
showCloseIcon: true));
|
||||
},
|
||||
highlightColor: Colors.transparent,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_rounded,
|
||||
color: Colors.red),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsHostInvalid(hostInvalidHost
|
||||
? "host"
|
||||
: "url"),
|
||||
style:
|
||||
const TextStyle(color: Colors.red))
|
||||
],
|
||||
))
|
||||
: null,
|
||||
helper: InkWell(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
highlightColor: Colors.transparent,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
child: hostLoading
|
||||
? Row(
|
||||
children: [
|
||||
const Icon(Icons.search_rounded,
|
||||
color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsHostChecking,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontFamily: "monospace"))
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
const Icon(Icons.check_rounded,
|
||||
color: Colors.green),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsHostValid,
|
||||
style: const TextStyle(
|
||||
color: Colors.green,
|
||||
fontFamily: "monospace"))
|
||||
],
|
||||
)))),
|
||||
title(AppLocalizations.of(context)!.settingsTitleBehavior,
|
||||
bottom: 24),
|
||||
TextField(
|
||||
controller: systemInputController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 2,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.settingsSystemMessage,
|
||||
hintText: "You are a helpful assistant",
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs?.setString(
|
||||
"system",
|
||||
(systemInputController.text.isNotEmpty)
|
||||
? systemInputController.text
|
||||
: "You are a helpful assistant");
|
||||
},
|
||||
icon: const Icon(Icons.save_rounded),
|
||||
),
|
||||
border: const OutlineInputBorder())),
|
||||
const SizedBox(height: 16),
|
||||
toggle(AppLocalizations.of(context)!.settingsDisableMarkdown,
|
||||
(prefs!.getBool("noMarkdown") ?? false), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("noMarkdown", value);
|
||||
setState(() {});
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
const Icon(Icons.warning_rounded, color: Colors.grey),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.settingsBehaviorNotUpdatedForOlderChats,
|
||||
style: const TextStyle(color: Colors.grey)))
|
||||
]),
|
||||
title(AppLocalizations.of(context)!.settingsTitleInterface),
|
||||
SegmentedButton(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: "stream",
|
||||
label: Text("Stream"),
|
||||
icon: Icon(Icons.stream_rounded)),
|
||||
ButtonSegment(
|
||||
value: "request",
|
||||
label: Text("Request"),
|
||||
icon: Icon(Icons.send_rounded))
|
||||
],
|
||||
selected: {
|
||||
prefs!.getString("requestType") ?? "stream"
|
||||
},
|
||||
onSelectionChanged: (p0) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() {
|
||||
prefs!.setString("requestType", p0.elementAt(0));
|
||||
});
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
toggle(AppLocalizations.of(context)!.settingsGenerateTitles,
|
||||
(prefs!.getBool("generateTitles") ?? true), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("generateTitles", value);
|
||||
setState(() {});
|
||||
}),
|
||||
toggle(AppLocalizations.of(context)!.settingsAskBeforeDelete,
|
||||
(prefs!.getBool("askBeforeDeletion") ?? false), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("askBeforeDeletion", value);
|
||||
setState(() {});
|
||||
}),
|
||||
toggle(AppLocalizations.of(context)!.settingsResetOnModelChange,
|
||||
(prefs!.getBool("resetOnModelSelect") ?? true), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("resetOnModelSelect", value);
|
||||
setState(() {});
|
||||
}),
|
||||
toggle(AppLocalizations.of(context)!.settingsEnableEditing,
|
||||
(prefs!.getBool("enableEditing") ?? false), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("enableEditing", value);
|
||||
setState(() {});
|
||||
}),
|
||||
toggle(AppLocalizations.of(context)!.settingsShowTips,
|
||||
(prefs!.getBool("tips") ?? true), (value) {
|
||||
HapticFeedback.selectionClick();
|
||||
prefs!.setBool("tips", value);
|
||||
setState(() {});
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
SegmentedButton(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: "dark",
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.settingsBrightnessDark),
|
||||
icon: const Icon(Icons.brightness_4_rounded)),
|
||||
ButtonSegment(
|
||||
value: "system",
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.settingsBrightnessSystem),
|
||||
icon: const Icon(Icons.brightness_auto_rounded)),
|
||||
ButtonSegment(
|
||||
value: "light",
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.settingsBrightnessLight),
|
||||
icon: const Icon(Icons.brightness_high_rounded))
|
||||
],
|
||||
selected: {
|
||||
prefs!.getString("brightness") ?? "system"
|
||||
},
|
||||
onSelectionChanged: (p0) {
|
||||
HapticFeedback.selectionClick();
|
||||
var tmp = prefs!.getString("brightness") ?? "system";
|
||||
prefs!.setString("brightness", p0.elementAt(0));
|
||||
setState(() {});
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setLocalState) {
|
||||
return PopScope(
|
||||
onPopInvoked: (didPop) {
|
||||
prefs!.setString("brightness", tmp);
|
||||
setState(() {});
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.settingsBrightnessRestartTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.settingsBrightnessRestartDescription),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(
|
||||
context)!
|
||||
.settingsBrightnessRestartCancel)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
HapticFeedback.selectionClick();
|
||||
await prefs!.setString(
|
||||
"brightness",
|
||||
p0.elementAt(0));
|
||||
Restart.restartApp();
|
||||
},
|
||||
child: Text(AppLocalizations.of(
|
||||
context)!
|
||||
.settingsBrightnessRestartRestart))
|
||||
]));
|
||||
});
|
||||
});
|
||||
}),
|
||||
title(AppLocalizations.of(context)!.settingsTitleContact),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
mode: LaunchMode.inAppBrowserView,
|
||||
Uri.parse(repoUrl));
|
||||
},
|
||||
child: Row(children: [
|
||||
const Icon(SimpleIcons.github),
|
||||
const SizedBox(width: 16, height: 42),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.settingsGithub))
|
||||
])),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
mode: LaunchMode.inAppBrowserView,
|
||||
Uri.parse("$repoUrl/issues"));
|
||||
},
|
||||
child: Row(children: [
|
||||
const Icon(Icons.report_rounded),
|
||||
const SizedBox(width: 16, height: 42),
|
||||
Expanded(
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.settingsReportIssue))
|
||||
])),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
launchUrl(
|
||||
mode: LaunchMode.inAppBrowserView,
|
||||
Uri.parse(
|
||||
repoUrl.substring(0, repoUrl.lastIndexOf('/'))));
|
||||
},
|
||||
child: Row(children: [
|
||||
const Icon(Icons.developer_board_rounded),
|
||||
const SizedBox(width: 16, height: 42),
|
||||
Expanded(
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.settingsMainDeveloper))
|
||||
])),
|
||||
const SizedBox(height: 16),
|
||||
]))),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'main.dart';
|
||||
|
||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
class ScreenWelcome extends StatefulWidget {
|
||||
@override
|
||||
State<ScreenWelcome> createState() => _ScreenWelcomeState();
|
||||
}
|
||||
|
||||
class _ScreenWelcomeState extends State<ScreenWelcome> {
|
||||
final _pageController = PageController();
|
||||
int page = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =
|
||||
() {
|
||||
// invert colors used, because brightness not updated yet
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[100])
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[100]),
|
||||
systemNavigationBarIconBrightness:
|
||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
};
|
||||
|
||||
// brightness changed function not run at first startup
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(prefs!.getString("brightness") ?? "system") == "system"
|
||||
? ((MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900])
|
||||
: (prefs!.getString("brightness") == "dark"
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[100]),
|
||||
systemNavigationBarIconBrightness:
|
||||
(((prefs!.getString("brightness") ?? "system") == "system" &&
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light) ||
|
||||
prefs!.getString("brightness") == "light")
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
precacheImage(const AssetImage("assets/welcome/1.png"), context);
|
||||
precacheImage(const AssetImage("assets/welcome/2.png"), context);
|
||||
precacheImage(const AssetImage("assets/welcome/3.png"), context);
|
||||
precacheImage(const AssetImage("assets/welcome/1dark.png"), context);
|
||||
precacheImage(const AssetImage("assets/welcome/2dark.png"), context);
|
||||
precacheImage(const AssetImage("assets/welcome/3dark.png"), context);
|
||||
return Scaffold(
|
||||
bottomNavigationBar: BottomSheet(
|
||||
enableDrag: false,
|
||||
backgroundColor: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
onClosing: () {},
|
||||
builder: (context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
SmoothPageIndicator(
|
||||
controller: _pageController,
|
||||
count: 3,
|
||||
effect: ExpandingDotsEffect(
|
||||
activeDotColor: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.primary
|
||||
: (themeDark ?? ThemeData.dark())
|
||||
.colorScheme
|
||||
.primary)),
|
||||
]));
|
||||
}),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (page < 2) {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut);
|
||||
} else {
|
||||
prefs!.setBool("welcomeFinished", true);
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? (theme ?? ThemeData()).colorScheme.surface
|
||||
: (themeDark ?? ThemeData.dark()).colorScheme.surface,
|
||||
systemNavigationBarIconBrightness:
|
||||
(Theme.of(context).brightness == Brightness.light)
|
||||
? Brightness.dark
|
||||
: Brightness.light));
|
||||
Navigator.pushReplacement(context,
|
||||
MaterialPageRoute(builder: (context) => const MainApp()));
|
||||
}
|
||||
},
|
||||
child: Icon((page < 2) ? Icons.arrow_forward : Icons.check_rounded),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (value) {
|
||||
setState(() {
|
||||
page = value;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
Center(
|
||||
child: (Theme.of(context).brightness == Brightness.light)
|
||||
? FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: const AssetImage("assets/welcome/1.png"))
|
||||
: FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image:
|
||||
const AssetImage("assets/welcome/1dark.png"))),
|
||||
Center(
|
||||
child: (Theme.of(context).brightness == Brightness.light)
|
||||
? FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: const AssetImage("assets/welcome/2.png"))
|
||||
: FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image:
|
||||
const AssetImage("assets/welcome/2dark.png"))),
|
||||
Center(
|
||||
child: (Theme.of(context).brightness == Brightness.light)
|
||||
? FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: const AssetImage("assets/welcome/3.png"))
|
||||
: FadeInImage(
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image:
|
||||
const AssetImage("assets/welcome/3dark.png")))
|
||||
])),
|
||||
])));
|
||||
}
|
||||
}
|
|
@ -1,127 +1,18 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'main.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
||||
|
||||
void setHost(BuildContext context, [bool force = true]) {
|
||||
bool loading = false;
|
||||
bool invalidHost = false;
|
||||
bool invalidUrl = false;
|
||||
final hostInputController =
|
||||
TextEditingController(text: prefs?.getString("host") ?? "");
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: !force,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => PopScope(
|
||||
canPop: !force,
|
||||
child: AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.hostDialogTitle),
|
||||
content: loading
|
||||
? const LinearProgressIndicator()
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.hostDialogDescription),
|
||||
invalidHost
|
||||
? Text(
|
||||
AppLocalizations.of(context)!
|
||||
.hostDialogErrorInvalidHost,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold))
|
||||
: const SizedBox.shrink(),
|
||||
invalidUrl
|
||||
? Text(
|
||||
AppLocalizations.of(context)!
|
||||
.hostDialogErrorInvalidUrl,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold))
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: hostInputController,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "http://example.com:8080"))
|
||||
]),
|
||||
actions: [
|
||||
!force
|
||||
? TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.hostDialogCancel))
|
||||
: const SizedBox.shrink(),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
invalidUrl = false;
|
||||
invalidHost = false;
|
||||
});
|
||||
var tmpHost = hostInputController.text
|
||||
.trim()
|
||||
.removeSuffix("/")
|
||||
.trim();
|
||||
|
||||
if (tmpHost.isEmpty) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var url = Uri.parse(tmpHost);
|
||||
if (!url.isAbsolute) {
|
||||
setState(() {
|
||||
invalidUrl = true;
|
||||
loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
http.Response request;
|
||||
try {
|
||||
request = await http.get(url).timeout(
|
||||
const Duration(seconds: 5), onTimeout: () {
|
||||
return http.Response('Error', 408);
|
||||
});
|
||||
} catch (e) {
|
||||
invalidHost = true;
|
||||
loading = false;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
if (request.statusCode != 200 ||
|
||||
request.body != "Ollama is running") {
|
||||
setState(() {
|
||||
invalidHost = true;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
messages = [];
|
||||
setState(() {});
|
||||
host = tmpHost;
|
||||
prefs?.setString("host", host!);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(AppLocalizations.of(context)!.hostDialogSave))
|
||||
]))));
|
||||
}
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
void setModel(BuildContext context, Function setState) {
|
||||
List<String> models = [];
|
||||
List<String> modelsReal = [];
|
||||
List<bool> modal = [];
|
||||
int usedIndex = -1;
|
||||
int addIndex = -1;
|
||||
|
@ -131,14 +22,15 @@ void setModel(BuildContext context, Function setState) {
|
|||
var list = await llama.OllamaClient(baseUrl: "$host/api").listModels();
|
||||
for (var i = 0; i < list.models!.length; i++) {
|
||||
models.add(list.models![i].model!.split(":")[0]);
|
||||
modelsReal.add(list.models![i].model!);
|
||||
modal.add((list.models![i].details!.families ?? []).contains("clip"));
|
||||
}
|
||||
addIndex = models.length;
|
||||
// ignore: use_build_context_synchronously
|
||||
models.add(AppLocalizations.of(context)!.modelDialogAddModel);
|
||||
modal.add(false);
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
if (models[i] == model) {
|
||||
for (var i = 0; i < modelsReal.length; i++) {
|
||||
if (modelsReal[i] == model) {
|
||||
usedIndex = i;
|
||||
}
|
||||
}
|
||||
|
@ -158,10 +50,13 @@ void setModel(BuildContext context, Function setState) {
|
|||
return PopScope(
|
||||
canPop: loaded,
|
||||
onPopInvoked: (didPop) {
|
||||
if (usedIndex >= 0 && models[usedIndex] != model) {
|
||||
if (!loaded) return;
|
||||
if (usedIndex >= 0 &&
|
||||
modelsReal[usedIndex] != model &&
|
||||
(prefs!.getBool("resetOnModelSelect") ?? true)) {
|
||||
messages = [];
|
||||
}
|
||||
model = (usedIndex >= 0) ? models[usedIndex] : null;
|
||||
model = (usedIndex >= 0) ? modelsReal[usedIndex] : null;
|
||||
multimodal = (usedIndex >= 0) ? modal[usedIndex] : false;
|
||||
if (model != null) {
|
||||
prefs?.setString("model", model!);
|
||||
|
@ -240,21 +135,13 @@ void setModel(BuildContext context, Function setState) {
|
|||
onSelected: (bool selected) {
|
||||
if (addIndex == index) {
|
||||
Navigator.of(context).pop();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16),
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.modelDialogAddSteps));
|
||||
});
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(
|
||||
context)!
|
||||
.modelDialogAddSteps),
|
||||
showCloseIcon: true));
|
||||
}
|
||||
if (!chatAllowed) return;
|
||||
setLocalState(() {
|
||||
|
@ -270,81 +157,217 @@ void setModel(BuildContext context, Function setState) {
|
|||
});
|
||||
}
|
||||
|
||||
void deleteChat(BuildContext context, Function setState) {
|
||||
if (prefs!.getBool("askBeforeDeletion") ?? true && messages.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, setLocalState) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteDialogTitle),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(AppLocalizations.of(context)!.deleteDialogDescription),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.deleteDialogAskAlways),
|
||||
const Expanded(child: SizedBox()),
|
||||
Switch(
|
||||
value: prefs!.getBool("askBeforeDeletion") ?? true,
|
||||
onChanged: (value) {
|
||||
prefs!.setBool("askBeforeDeletion", value);
|
||||
setLocalState(() {});
|
||||
},
|
||||
)
|
||||
])
|
||||
]),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.deleteDialogCancel)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop();
|
||||
messages = [];
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.deleteDialogDelete))
|
||||
]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
messages = [];
|
||||
setState(() {});
|
||||
void saveChat(String uuid, Function setState) {
|
||||
int index = -1;
|
||||
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == uuid) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index == -1) return;
|
||||
List<Map<String, String>> history = [];
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
history.add({
|
||||
"role": (messages[i].author == user) ? "user" : "assistant",
|
||||
"content": jsonDecode(jsonEncode(messages[i]))["text"]
|
||||
});
|
||||
}
|
||||
if (messages.isEmpty && uuid == chatUuid) {
|
||||
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
||||
chatUuid) {
|
||||
List<String> tmp = prefs!.getStringList("chats")!;
|
||||
tmp.removeAt(i);
|
||||
prefs!.setStringList("chats", tmp);
|
||||
chatUuid = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[index])["messages"]
|
||||
.length >=
|
||||
1) {
|
||||
if (jsonDecode(jsonDecode((prefs!.getStringList("chats") ?? [])[index])[
|
||||
"messages"])[0]["role"] ==
|
||||
"system") {
|
||||
history.add({
|
||||
"role": "system",
|
||||
"content": jsonDecode(jsonDecode(
|
||||
(prefs!.getStringList("chats") ?? [])[index])["messages"])[0]
|
||||
["content"]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var system = prefs?.getString("system") ?? "You are a helpful assistant";
|
||||
if (prefs!.getBool("noMarkdown") ?? false) {
|
||||
system +=
|
||||
" You must not use markdown or any other formatting language in any way!";
|
||||
}
|
||||
history.add({"role": "system", "content": system});
|
||||
}
|
||||
history = history.reversed.toList();
|
||||
List<String> tmp = prefs!.getStringList("chats") ?? [];
|
||||
tmp.removeAt(index);
|
||||
tmp.insert(
|
||||
0,
|
||||
jsonEncode({
|
||||
"title":
|
||||
jsonDecode((prefs!.getStringList("chats") ?? [])[index])["title"],
|
||||
"uuid": uuid,
|
||||
"model": model,
|
||||
"messages": jsonEncode(history)
|
||||
}));
|
||||
prefs!.setStringList("chats", tmp);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setAskBeforeDeletion(BuildContext context, Function setState) {
|
||||
showDialog(
|
||||
void loadChat(String uuid, Function setState) {
|
||||
int index = -1;
|
||||
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
||||
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == uuid) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (index == -1) return;
|
||||
messages = [];
|
||||
model = null;
|
||||
setState(() {});
|
||||
var history = jsonDecode(
|
||||
jsonDecode((prefs!.getStringList("chats") ?? [])[index])["messages"]);
|
||||
for (var i = 0; i < history.length; i++) {
|
||||
if (history[i]["role"] != "system") {
|
||||
messages.insert(
|
||||
0,
|
||||
types.TextMessage(
|
||||
author: (history[i]["role"] == "user") ? user : assistant,
|
||||
id: const Uuid().v4(),
|
||||
text: history[i]["content"]));
|
||||
}
|
||||
}
|
||||
model = jsonDecode((prefs!.getStringList("chats") ?? [])[index])["model"];
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<String> prompt(BuildContext context,
|
||||
{String description = "",
|
||||
String value = "",
|
||||
String title = "",
|
||||
bool force = false,
|
||||
String? valueIfCanceled,
|
||||
TextInputType keyboard = TextInputType.text,
|
||||
Icon? prefixIcon,
|
||||
int maxLines = 1,
|
||||
String? uuid}) async {
|
||||
var returnText = (valueIfCanceled != null) ? valueIfCanceled : value;
|
||||
final TextEditingController controller = TextEditingController(text: value);
|
||||
bool loading = false;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, setLocalState) {
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
return PopScope(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!
|
||||
.deleteDialogAskAlways),
|
||||
const Expanded(child: SizedBox()),
|
||||
Switch(
|
||||
value: prefs!.getBool("askBeforeDeletion") ?? true,
|
||||
onChanged: (value) {
|
||||
prefs!.setBool("askBeforeDeletion", value);
|
||||
setLocalState(() {});
|
||||
},
|
||||
)
|
||||
(title != "")
|
||||
? Text(title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold))
|
||||
: const SizedBox.shrink(),
|
||||
(title != "")
|
||||
? const Divider()
|
||||
: const SizedBox.shrink(),
|
||||
(description != "")
|
||||
? Text(description)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: keyboard,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
returnText = controller.text;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.save_rounded)),
|
||||
prefixIcon: (title ==
|
||||
AppLocalizations.of(context)!
|
||||
.dialogEnterNewTitle &&
|
||||
uuid != null)
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
setLocalState(() {
|
||||
loading = true;
|
||||
});
|
||||
for (var i = 0;
|
||||
i <
|
||||
(prefs!.getStringList(
|
||||
"chats") ??
|
||||
[])
|
||||
.length;
|
||||
i++) {
|
||||
if (jsonDecode((prefs!
|
||||
.getStringList(
|
||||
"chats") ??
|
||||
[])[i])["uuid"] ==
|
||||
uuid) {
|
||||
try {
|
||||
var history = jsonDecode(
|
||||
jsonDecode((prefs!
|
||||
.getStringList(
|
||||
"chats") ??
|
||||
[])[i])["messages"]);
|
||||
|
||||
final generated =
|
||||
await llama.OllamaClient(
|
||||
baseUrl:
|
||||
"$host/api")
|
||||
.generateCompletion(
|
||||
request: llama
|
||||
.GenerateCompletionRequest(
|
||||
model: model!,
|
||||
prompt:
|
||||
"You must not use markdown or any other formatting language! Create a short title for the subject of the conversation described in the following json object. It is not allowed to be too general; no 'Assistance', 'Help' or similar!\n\n```json\n${jsonEncode(history)}\n```",
|
||||
),
|
||||
);
|
||||
var title = generated.response!
|
||||
.replaceAll("*", "")
|
||||
.replaceAll("_", "");
|
||||
controller.text = title;
|
||||
setLocalState(() {
|
||||
loading = false;
|
||||
});
|
||||
} catch (_) {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.auto_awesome_rounded))
|
||||
: prefixIcon)),
|
||||
SizedBox(
|
||||
height: 3,
|
||||
child: (loading)
|
||||
? const LinearProgressIndicator()
|
||||
: const SizedBox.shrink()),
|
||||
(MediaQuery.of(context).viewInsets.bottom != 0)
|
||||
? const SizedBox(height: 16)
|
||||
: const SizedBox.shrink()
|
||||
])));
|
||||
});
|
||||
});
|
||||
return returnText;
|
||||
}
|
||||
|
|
67
pubspec.lock
|
@ -9,6 +9,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.2"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -227,6 +235,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
flutter_parsed_text:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -405,6 +421,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -440,10 +464,11 @@ packages:
|
|||
ollama_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ollama_dart
|
||||
sha256: "5e83b6b77785e7dbc454ff70ab14883e6cc1e6157c8df4e84da77845bc074df9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "packages/ollama_dart"
|
||||
ref: ce2ef30c9a9a0dfe8f3059988b7007c94c45b9bd
|
||||
resolved-ref: ce2ef30c9a9a0dfe8f3059988b7007c94c45b9bd
|
||||
url: "https://github.com/davidmigloz/langchain_dart.git"
|
||||
source: git
|
||||
version: "0.1.0+1"
|
||||
path:
|
||||
dependency: transitive
|
||||
|
@ -501,6 +526,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
restart_app:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: restart_app
|
||||
sha256: b37daeb1c02fcab30e19d9e30b6fdd215bd53577efd927042eb77cf6f09daadb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
scroll_to_index:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -565,11 +598,27 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
simple_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: simple_icons
|
||||
sha256: "30067d70a9d72923fbc80e142e17fa46085dfa970e66bc4bede3be4819d05901"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
smooth_page_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: smooth_page_indicator
|
||||
sha256: "725bc638d5e79df0c84658e1291449996943f93bacbc2cec49963dbbab48d8ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -634,6 +683,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
transparent_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: transparent_image
|
||||
sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -643,7 +700,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.3.2"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||
|
|
21
pubspec.yaml
|
@ -1,7 +1,7 @@
|
|||
name: ollama_app
|
||||
description: "A modern and easy-to-use client for Ollama"
|
||||
publish_to: 'none'
|
||||
version: 0.0.2
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.4 <4.0.0'
|
||||
|
@ -20,7 +20,17 @@ dependencies:
|
|||
intl: any
|
||||
http: ^1.2.1
|
||||
dartx: ^1.2.0
|
||||
ollama_dart: ^0.1.0+1
|
||||
ollama_dart:
|
||||
git:
|
||||
url: https://github.com/davidmigloz/langchain_dart.git
|
||||
path: packages/ollama_dart
|
||||
ref: ce2ef30c9a9a0dfe8f3059988b7007c94c45b9bd
|
||||
smooth_page_indicator: ^1.1.0
|
||||
transparent_image: ^2.0.1
|
||||
simple_icons: ^10.1.3
|
||||
url_launcher: ^6.2.6
|
||||
restart_app: ^1.2.1
|
||||
flutter_markdown: ^0.7.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -32,3 +42,10 @@ flutter:
|
|||
generate: true
|
||||
assets:
|
||||
- assets/logo512.png
|
||||
- assets/logo512error.png
|
||||
- assets/welcome/1.png
|
||||
- assets/welcome/2.png
|
||||
- assets/welcome/3.png
|
||||
- assets/welcome/1dark.png
|
||||
- assets/welcome/2dark.png
|
||||
- assets/welcome/3dark.png
|
||||
|
|