mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-09-11 13:25:35 +03:00
feat: add openai-compatible api models (#2683)
Signed-off-by: Shiranui <supersonic@livemail.tw> Signed-off-by: Jared Van Bortel <jared@nomic.ai> Co-authored-by: Jared Van Bortel <jared@nomic.ai>
This commit is contained in:
parent
6b97d0f8ea
commit
f9cd2e321c
@ -153,6 +153,8 @@ qt_add_qml_module(chat
|
||||
qml/SwitchModelDialog.qml
|
||||
qml/Theme.qml
|
||||
qml/ThumbsDownDialog.qml
|
||||
qml/Toast.qml
|
||||
qml/ToastManager.qml
|
||||
qml/MyBusyIndicator.qml
|
||||
qml/MyButton.qml
|
||||
qml/MyCheckBox.qml
|
||||
|
@ -201,6 +201,11 @@ void ChatAPIWorker::request(const QString &apiKey,
|
||||
QNetworkRequest request(apiUrl);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization", authorization.toUtf8());
|
||||
#if defined(DEBUG)
|
||||
qDebug() << "ChatAPI::request"
|
||||
<< "API URL: " << apiUrl.toString()
|
||||
<< "Authorization: " << authorization.toUtf8();
|
||||
#endif
|
||||
m_networkManager = new QNetworkAccessManager(this);
|
||||
QNetworkReply *reply = m_networkManager->post(request, array);
|
||||
connect(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
|
||||
@ -218,10 +223,28 @@ void ChatAPIWorker::handleFinished()
|
||||
}
|
||||
|
||||
QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
Q_ASSERT(response.isValid());
|
||||
|
||||
if (!response.isValid()) {
|
||||
m_chat->callResponse(
|
||||
-1,
|
||||
tr("ERROR: Network error occurred while connecting to the API server")
|
||||
.toStdString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
int code = response.toInt(&ok);
|
||||
if (!ok || code != 200) {
|
||||
bool isReplyEmpty(reply->readAll().isEmpty());
|
||||
if (isReplyEmpty)
|
||||
m_chat->callResponse(
|
||||
-1,
|
||||
tr("ChatAPIWorker::handleFinished got HTTP Error %1 %2")
|
||||
.arg(code)
|
||||
.arg(reply->errorString())
|
||||
.toStdString()
|
||||
);
|
||||
qWarning().noquote() << "ERROR: ChatAPIWorker::handleFinished got HTTP Error" << code << "response:"
|
||||
<< reply->errorString();
|
||||
}
|
||||
@ -238,7 +261,10 @@ void ChatAPIWorker::handleReadyRead()
|
||||
}
|
||||
|
||||
QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
Q_ASSERT(response.isValid());
|
||||
|
||||
if (!response.isValid())
|
||||
return;
|
||||
|
||||
bool ok;
|
||||
int code = response.toInt(&ok);
|
||||
if (!ok || code != 200) {
|
||||
|
@ -322,6 +322,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
QVariantMap modelLoadProps;
|
||||
if (modelInfo.isOnline) {
|
||||
QString apiKey;
|
||||
QString requestUrl;
|
||||
QString modelName;
|
||||
{
|
||||
QFile file(filePath);
|
||||
@ -332,11 +333,24 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
QJsonObject obj = doc.object();
|
||||
apiKey = obj["apiKey"].toString();
|
||||
modelName = obj["modelName"].toString();
|
||||
if (modelInfo.isCompatibleApi) {
|
||||
QString baseUrl(obj["baseUrl"].toString());
|
||||
QUrl apiUrl(QUrl::fromUserInput(baseUrl));
|
||||
if (!Network::isHttpUrlValid(apiUrl)) {
|
||||
return false;
|
||||
}
|
||||
QString currentPath(apiUrl.path());
|
||||
QString suffixPath("%1/chat/completions");
|
||||
apiUrl.setPath(suffixPath.arg(currentPath));
|
||||
requestUrl = apiUrl.toString();
|
||||
} else {
|
||||
requestUrl = modelInfo.url();
|
||||
}
|
||||
}
|
||||
m_llModelType = LLModelType::API_;
|
||||
ChatAPI *model = new ChatAPI();
|
||||
model->setModelName(modelName);
|
||||
model->setRequestURL(modelInfo.url());
|
||||
model->setRequestURL(requestUrl);
|
||||
model->setAPIKey(apiKey);
|
||||
m_llModelInfo.resetModel(this, model);
|
||||
} else if (!loadNewModel(modelInfo, modelLoadProps)) {
|
||||
|
@ -237,6 +237,54 @@ void Download::installModel(const QString &modelFile, const QString &apiKey)
|
||||
stream << doc.toJson();
|
||||
file.close();
|
||||
ModelList::globalInstance()->updateModelsFromDirectory();
|
||||
emit toastMessage(tr("Model \"%1\" is installed successfully.").arg(modelName));
|
||||
}
|
||||
|
||||
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
|
||||
}
|
||||
|
||||
void Download::installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl)
|
||||
{
|
||||
Q_ASSERT(!modelName.isEmpty());
|
||||
if (modelName.isEmpty()) {
|
||||
emit toastMessage(tr("ERROR: $MODEL_NAME is empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
Q_ASSERT(!apiKey.isEmpty());
|
||||
if (apiKey.isEmpty()) {
|
||||
emit toastMessage(tr("ERROR: $API_KEY is empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl apiBaseUrl(QUrl::fromUserInput(baseUrl));
|
||||
if (!Network::isHttpUrlValid(baseUrl)) {
|
||||
emit toastMessage(tr("ERROR: $BASE_URL is invalid."));
|
||||
return;
|
||||
}
|
||||
|
||||
QString modelFile(ModelList::compatibleModelFilename(baseUrl, modelName));
|
||||
if (ModelList::globalInstance()->contains(modelFile)) {
|
||||
emit toastMessage(tr("ERROR: Model \"%1 (%2)\" is conflict.").arg(modelName, baseUrl));
|
||||
return;
|
||||
}
|
||||
ModelList::globalInstance()->addModel(modelFile);
|
||||
Network::globalInstance()->trackEvent("install_model", { {"model", modelFile} });
|
||||
|
||||
QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
|
||||
QJsonObject obj;
|
||||
obj.insert("apiKey", apiKey);
|
||||
obj.insert("modelName", modelName);
|
||||
obj.insert("baseUrl", apiBaseUrl.toString());
|
||||
QJsonDocument doc(obj);
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream << doc.toJson();
|
||||
file.close();
|
||||
ModelList::globalInstance()->updateModelsFromDirectory();
|
||||
emit toastMessage(tr("Model \"%1 (%2)\" is installed successfully.").arg(modelName, baseUrl));
|
||||
}
|
||||
|
||||
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
|
||||
@ -255,11 +303,12 @@ void Download::removeModel(const QString &modelFile)
|
||||
if (file.exists()) {
|
||||
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
|
||||
MySettings::globalInstance()->eraseModel(info);
|
||||
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.description() == "" /*indicates sideloaded*/);
|
||||
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.isCompatibleApi || info.description() == "" /*indicates sideloaded*/);
|
||||
if (shouldRemoveInstalled)
|
||||
ModelList::globalInstance()->removeInstalled(info);
|
||||
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
|
||||
file.remove();
|
||||
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
|
||||
}
|
||||
|
||||
if (!shouldRemoveInstalled) {
|
||||
|
@ -63,6 +63,7 @@ public:
|
||||
Q_INVOKABLE void downloadModel(const QString &modelFile);
|
||||
Q_INVOKABLE void cancelDownload(const QString &modelFile);
|
||||
Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey);
|
||||
Q_INVOKABLE void installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl);
|
||||
Q_INVOKABLE void removeModel(const QString &modelFile);
|
||||
Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const;
|
||||
|
||||
@ -87,6 +88,7 @@ Q_SIGNALS:
|
||||
void requestHashAndSave(const QString &hash, QCryptographicHash::Algorithm a, const QString &saveFilePath,
|
||||
QFile *tempFile, QNetworkReply *modelReply);
|
||||
void latestNewsChanged();
|
||||
void toastMessage(const QString &message);
|
||||
|
||||
private:
|
||||
void parseReleaseJsonFile(const QByteArray &jsonData);
|
||||
|
@ -514,6 +514,17 @@ ModelList::ModelList()
|
||||
QCoreApplication::instance()->installEventFilter(this);
|
||||
}
|
||||
|
||||
QString ModelList::compatibleModelNameHash(QUrl baseUrl, QString modelName) {
|
||||
QCryptographicHash sha256(QCryptographicHash::Sha256);
|
||||
sha256.addData((baseUrl.toString() + "_" + modelName).toUtf8());
|
||||
return sha256.result().toHex();
|
||||
};
|
||||
|
||||
QString ModelList::compatibleModelFilename(QUrl baseUrl, QString modelName) {
|
||||
QString hash(compatibleModelNameHash(baseUrl, modelName));
|
||||
return QString(u"gpt4all-%1-capi.rmodel"_s).arg(hash);
|
||||
};
|
||||
|
||||
bool ModelList::eventFilter(QObject *obj, QEvent *ev)
|
||||
{
|
||||
if (obj == QCoreApplication::instance() && ev->type() == QEvent::LanguageChange)
|
||||
@ -703,6 +714,8 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
|
||||
return info->isDefault;
|
||||
case OnlineRole:
|
||||
return info->isOnline;
|
||||
case CompatibleApiRole:
|
||||
return info->isCompatibleApi;
|
||||
case DescriptionRole:
|
||||
return info->description();
|
||||
case RequiresVersionRole:
|
||||
@ -859,6 +872,8 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
|
||||
info->isDefault = value.toBool(); break;
|
||||
case OnlineRole:
|
||||
info->isOnline = value.toBool(); break;
|
||||
case CompatibleApiRole:
|
||||
info->isCompatibleApi = value.toBool(); break;
|
||||
case DescriptionRole:
|
||||
info->setDescription(value.toString()); break;
|
||||
case RequiresVersionRole:
|
||||
@ -1079,6 +1094,7 @@ QString ModelList::clone(const ModelInfo &model)
|
||||
{ ModelList::FilenameRole, model.filename() },
|
||||
{ ModelList::DirpathRole, model.dirpath },
|
||||
{ ModelList::OnlineRole, model.isOnline },
|
||||
{ ModelList::CompatibleApiRole, model.isCompatibleApi },
|
||||
{ ModelList::IsEmbeddingModelRole, model.isEmbeddingModel },
|
||||
{ ModelList::TemperatureRole, model.temperature() },
|
||||
{ ModelList::TopPRole, model.topP() },
|
||||
@ -1113,7 +1129,7 @@ void ModelList::removeInstalled(const ModelInfo &model)
|
||||
{
|
||||
Q_ASSERT(model.installed);
|
||||
Q_ASSERT(!model.isClone());
|
||||
Q_ASSERT(model.isDiscovered() || model.description() == "" /*indicates sideloaded*/);
|
||||
Q_ASSERT(model.isDiscovered() || model.isCompatibleApi || model.description() == "" /*indicates sideloaded*/);
|
||||
removeInternal(model);
|
||||
emit layoutChanged();
|
||||
}
|
||||
@ -1260,14 +1276,53 @@ void ModelList::updateModelsFromDirectory()
|
||||
|
||||
QFileInfo info = it.fileInfo();
|
||||
|
||||
bool isOnline(filename.endsWith(".rmodel"));
|
||||
bool isCompatibleApi(filename.endsWith("-capi.rmodel"));
|
||||
|
||||
QString name;
|
||||
QString description;
|
||||
if (isCompatibleApi) {
|
||||
QJsonObject obj;
|
||||
{
|
||||
QFile file(path + filename);
|
||||
bool success = file.open(QIODeviceBase::ReadOnly);
|
||||
(void)success;
|
||||
Q_ASSERT(success);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
obj = doc.object();
|
||||
}
|
||||
{
|
||||
QString apiKey(obj["apiKey"].toString());
|
||||
QString baseUrl(obj["baseUrl"].toString());
|
||||
QString modelName(obj["modelName"].toString());
|
||||
apiKey = apiKey.length() < 10 ? "*****" : apiKey.left(5) + "*****";
|
||||
name = tr("%1 (%2)").arg(modelName, baseUrl);
|
||||
description = tr("<strong>OpenAI-Compatible API Model</strong><br>"
|
||||
"<ul><li>API Key: %1</li>"
|
||||
"<li>Base URL: %2</li>"
|
||||
"<li>Model Name: %3</li></ul>")
|
||||
.arg(apiKey, baseUrl, modelName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &id : modelsById) {
|
||||
QVector<QPair<int, QVariant>> data {
|
||||
{ InstalledRole, true },
|
||||
{ FilenameRole, filename },
|
||||
{ OnlineRole, filename.endsWith(".rmodel") },
|
||||
{ OnlineRole, isOnline },
|
||||
{ CompatibleApiRole, isCompatibleApi },
|
||||
{ DirpathRole, info.dir().absolutePath() + "/" },
|
||||
{ FilesizeRole, toFileSize(info.size()) },
|
||||
};
|
||||
if (isCompatibleApi) {
|
||||
// The data will be saved to "GPT4All.ini".
|
||||
data.append({ NameRole, name });
|
||||
// The description is hard-coded into "GPT4All.ini" due to performance issue.
|
||||
// If the description goes to be dynamic from its .rmodel file, it will get high I/O usage while using the ModelList.
|
||||
data.append({ DescriptionRole, description });
|
||||
// Prompt template should be clear while using ChatML format which is using in most of OpenAI-Compatible API server.
|
||||
data.append({ PromptTemplateRole, "%1" });
|
||||
}
|
||||
updateData(id, data);
|
||||
}
|
||||
}
|
||||
@ -1657,6 +1712,34 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
|
||||
};
|
||||
updateData(id, data);
|
||||
}
|
||||
|
||||
const QString compatibleDesc = tr("<ul><li>Requires personal API key and the API base URL.</li>"
|
||||
"<li>WARNING: Will send your chats to "
|
||||
"the OpenAI-compatible API Server you specified!</li>"
|
||||
"<li>Your API key will be stored on disk</li><li>Will only be used"
|
||||
" to communicate with the OpenAI-compatible API Server</li>");
|
||||
|
||||
{
|
||||
const QString modelName = "OpenAI-compatible";
|
||||
const QString id = modelName;
|
||||
if (!contains(id))
|
||||
addModel(id);
|
||||
QVector<QPair<int, QVariant>> data {
|
||||
{ ModelList::NameRole, modelName },
|
||||
{ ModelList::FilesizeRole, "minimal" },
|
||||
{ ModelList::OnlineRole, true },
|
||||
{ ModelList::CompatibleApiRole, true },
|
||||
{ ModelList::DescriptionRole,
|
||||
tr("<strong>Connect to OpenAI-compatible API server</strong><br> %1").arg(compatibleDesc) },
|
||||
{ ModelList::RequiresVersionRole, "2.7.4" },
|
||||
{ ModelList::OrderRole, "cf" },
|
||||
{ ModelList::RamrequiredRole, 0 },
|
||||
{ ModelList::ParametersRole, "?" },
|
||||
{ ModelList::QuantRole, "NA" },
|
||||
{ ModelList::TypeRole, "NA" },
|
||||
};
|
||||
updateData(id, data);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelList::updateDiscoveredInstalled(const ModelInfo &info)
|
||||
|
@ -35,6 +35,7 @@ struct ModelInfo {
|
||||
Q_PROPERTY(bool installed MEMBER installed)
|
||||
Q_PROPERTY(bool isDefault MEMBER isDefault)
|
||||
Q_PROPERTY(bool isOnline MEMBER isOnline)
|
||||
Q_PROPERTY(bool isCompatibleApi MEMBER isCompatibleApi)
|
||||
Q_PROPERTY(QString description READ description WRITE setDescription)
|
||||
Q_PROPERTY(QString requiresVersion MEMBER requiresVersion)
|
||||
Q_PROPERTY(QString versionRemoved MEMBER versionRemoved)
|
||||
@ -123,7 +124,17 @@ public:
|
||||
bool calcHash = false;
|
||||
bool installed = false;
|
||||
bool isDefault = false;
|
||||
// Differences between 'isOnline' and 'isCompatibleApi' in ModelInfo:
|
||||
// 'isOnline':
|
||||
// - Indicates whether this is a online model.
|
||||
// - Linked with the ModelList, fetching info from it.
|
||||
bool isOnline = false;
|
||||
// 'isCompatibleApi':
|
||||
// - Indicates whether the model is using the OpenAI-compatible API which user custom.
|
||||
// - When the property is true, 'isOnline' should also be true.
|
||||
// - Does not link to the ModelList directly; instead, fetches info from the *-capi.rmodel file and works standalone.
|
||||
// - Still needs to copy data from gpt4all.ini and *-capi.rmodel to the ModelList in memory while application getting started(as custom .gguf models do).
|
||||
bool isCompatibleApi = false;
|
||||
QString requiresVersion;
|
||||
QString versionRemoved;
|
||||
qint64 bytesReceived = 0;
|
||||
@ -276,6 +287,9 @@ class ModelList : public QAbstractListModel
|
||||
public:
|
||||
static ModelList *globalInstance();
|
||||
|
||||
static QString compatibleModelNameHash(QUrl baseUrl, QString modelName);
|
||||
static QString compatibleModelFilename(QUrl baseUrl, QString modelName);
|
||||
|
||||
enum DiscoverSort {
|
||||
Default,
|
||||
Likes,
|
||||
@ -295,6 +309,7 @@ public:
|
||||
InstalledRole,
|
||||
DefaultRole,
|
||||
OnlineRole,
|
||||
CompatibleApiRole,
|
||||
DescriptionRole,
|
||||
RequiresVersionRole,
|
||||
VersionRemovedRole,
|
||||
@ -347,6 +362,7 @@ public:
|
||||
roles[InstalledRole] = "installed";
|
||||
roles[DefaultRole] = "isDefault";
|
||||
roles[OnlineRole] = "isOnline";
|
||||
roles[CompatibleApiRole] = "isCompatibleApi";
|
||||
roles[DescriptionRole] = "description";
|
||||
roles[RequiresVersionRole] = "requiresVersion";
|
||||
roles[VersionRemovedRole] = "versionRemoved";
|
||||
|
@ -99,6 +99,15 @@ Network *Network::globalInstance()
|
||||
return networkInstance();
|
||||
}
|
||||
|
||||
bool Network::isHttpUrlValid(QUrl url) {
|
||||
if (!url.isValid())
|
||||
return false;
|
||||
QString scheme(url.scheme());
|
||||
if (scheme != "http" && scheme != "https")
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Network::Network()
|
||||
: QObject{nullptr}
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ class Network : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
static Network *globalInstance();
|
||||
static bool isHttpUrlValid(const QUrl url);
|
||||
|
||||
Q_INVOKABLE QString generateUniqueId() const;
|
||||
Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation);
|
||||
|
@ -25,6 +25,10 @@ Rectangle {
|
||||
color: theme.viewBackground
|
||||
signal modelsViewRequested()
|
||||
|
||||
ToastManager {
|
||||
id: messageToast
|
||||
}
|
||||
|
||||
PopupDialog {
|
||||
id: downloadingErrorPopup
|
||||
anchors.centerIn: parent
|
||||
@ -437,10 +441,35 @@ Rectangle {
|
||||
text: qsTr("Install")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
onClicked: {
|
||||
if (apiKey.text === "")
|
||||
var apiKeyText = apiKey.text.trim(),
|
||||
baseUrlText = baseUrl.text.trim(),
|
||||
modelNameText = modelName.text.trim();
|
||||
|
||||
var apiKeyOk = apiKeyText !== "",
|
||||
baseUrlOk = !isCompatibleApi || baseUrlText !== "",
|
||||
modelNameOk = !isCompatibleApi || modelNameText !== "";
|
||||
|
||||
if (!apiKeyOk)
|
||||
apiKey.showError();
|
||||
if (!baseUrlOk)
|
||||
baseUrl.showError();
|
||||
if (!modelNameOk)
|
||||
modelName.showError();
|
||||
|
||||
if (!apiKeyOk || !baseUrlOk || !modelNameOk)
|
||||
return;
|
||||
|
||||
if (!isCompatibleApi)
|
||||
Download.installModel(
|
||||
filename,
|
||||
apiKeyText,
|
||||
);
|
||||
else
|
||||
Download.installModel(filename, apiKey.text);
|
||||
Download.installCompatibleModel(
|
||||
modelNameText,
|
||||
apiKeyText,
|
||||
baseUrlText,
|
||||
);
|
||||
}
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Install")
|
||||
@ -571,16 +600,59 @@ Rectangle {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
apiKey.placeholderTextColor = theme.textErrorColor
|
||||
messageToast.show(qsTr("ERROR: $API_KEY is empty."));
|
||||
apiKey.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
apiKey.placeholderTextColor = theme.mutedTextColor
|
||||
apiKey.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $API_KEY")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
|
||||
MyTextField {
|
||||
id: baseUrl
|
||||
visible: !installed && isOnline && isCompatibleApi
|
||||
Layout.topMargin: 20
|
||||
Layout.leftMargin: 20
|
||||
Layout.minimumWidth: 200
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
messageToast.show(qsTr("ERROR: $BASE_URL is empty."));
|
||||
baseUrl.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
baseUrl.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $BASE_URL")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
|
||||
MyTextField {
|
||||
id: modelName
|
||||
visible: !installed && isOnline && isCompatibleApi
|
||||
Layout.topMargin: 20
|
||||
Layout.leftMargin: 20
|
||||
Layout.minimumWidth: 200
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
messageToast.show(qsTr("ERROR: $MODEL_NAME is empty."))
|
||||
modelName.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
modelName.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $MODEL_NAME")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -718,4 +790,11 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Download
|
||||
function onToastMessage(message) {
|
||||
messageToast.show(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -696,6 +696,7 @@ Rectangle {
|
||||
rightPadding: 60
|
||||
leftPadding: 60
|
||||
property string defaultModel: ""
|
||||
property string defaultModelName: ""
|
||||
function updateDefaultModel() {
|
||||
var i = comboBox.find(MySettings.userDefaultModel)
|
||||
if (i !== -1) {
|
||||
@ -703,9 +704,14 @@ Rectangle {
|
||||
} else {
|
||||
defaultModel = comboBox.valueAt(0);
|
||||
}
|
||||
if (defaultModel !== "") {
|
||||
defaultModelName = ModelList.modelInfo(defaultModel).name;
|
||||
} else {
|
||||
defaultModelName = "";
|
||||
}
|
||||
}
|
||||
|
||||
text: qsTr("Load \u00B7 %1 (default) \u2192").arg(defaultModel);
|
||||
text: qsTr("Load \u00B7 %1 (default) \u2192").arg(defaultModelName);
|
||||
onClicked: {
|
||||
var i = comboBox.find(MySettings.userDefaultModel)
|
||||
if (i !== -1) {
|
||||
|
@ -17,6 +17,10 @@ Rectangle {
|
||||
|
||||
signal addModelViewRequested()
|
||||
|
||||
ToastManager {
|
||||
id: messageToast
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
@ -233,10 +237,35 @@ Rectangle {
|
||||
text: qsTr("Install")
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
onClicked: {
|
||||
if (apiKey.text === "")
|
||||
var apiKeyText = apiKey.text.trim(),
|
||||
baseUrlText = baseUrl.text.trim(),
|
||||
modelNameText = modelName.text.trim();
|
||||
|
||||
var apiKeyOk = apiKeyText !== "",
|
||||
baseUrlOk = !isCompatibleApi || baseUrlText !== "",
|
||||
modelNameOk = !isCompatibleApi || modelNameText !== "";
|
||||
|
||||
if (!apiKeyOk)
|
||||
apiKey.showError();
|
||||
if (!baseUrlOk)
|
||||
baseUrl.showError();
|
||||
if (!modelNameOk)
|
||||
modelName.showError();
|
||||
|
||||
if (!apiKeyOk || !baseUrlOk || !modelNameOk)
|
||||
return;
|
||||
|
||||
if (!isCompatibleApi)
|
||||
Download.installModel(
|
||||
filename,
|
||||
apiKeyText,
|
||||
);
|
||||
else
|
||||
Download.installModel(filename, apiKey.text);
|
||||
Download.installCompatibleModel(
|
||||
modelNameText,
|
||||
apiKeyText,
|
||||
baseUrlText,
|
||||
);
|
||||
}
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Install")
|
||||
@ -367,16 +396,59 @@ Rectangle {
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
apiKey.placeholderTextColor = theme.textErrorColor
|
||||
messageToast.show(qsTr("ERROR: $API_KEY is empty."));
|
||||
apiKey.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
apiKey.placeholderTextColor = theme.mutedTextColor
|
||||
apiKey.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $API_KEY")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
|
||||
MyTextField {
|
||||
id: baseUrl
|
||||
visible: !installed && isOnline && isCompatibleApi
|
||||
Layout.topMargin: 20
|
||||
Layout.leftMargin: 20
|
||||
Layout.minimumWidth: 200
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
messageToast.show(qsTr("ERROR: $BASE_URL is empty."));
|
||||
baseUrl.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
baseUrl.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $BASE_URL")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
|
||||
MyTextField {
|
||||
id: modelName
|
||||
visible: !installed && isOnline && isCompatibleApi
|
||||
Layout.topMargin: 20
|
||||
Layout.leftMargin: 20
|
||||
Layout.minimumWidth: 200
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
wrapMode: Text.WrapAnywhere
|
||||
function showError() {
|
||||
messageToast.show(qsTr("ERROR: $MODEL_NAME is empty."))
|
||||
modelName.placeholderTextColor = theme.textErrorColor;
|
||||
}
|
||||
onTextChanged: {
|
||||
modelName.placeholderTextColor = theme.mutedTextColor;
|
||||
}
|
||||
placeholderText: qsTr("enter $MODEL_NAME")
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.name: placeholderText
|
||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -514,4 +586,11 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Download
|
||||
function onToastMessage(message) {
|
||||
messageToast.show(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
gpt4all-chat/qml/Toast.qml
Normal file
98
gpt4all-chat/qml/Toast.qml
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Source: https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129
|
||||
* Adapted from StackOverflow: http://stackoverflow.com/questions/26879266/make-toast-in-android-by-qml
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
|
||||
/**
|
||||
* @brief An Android-like timed message text in a box that self-destroys when finished if desired
|
||||
*/
|
||||
Rectangle {
|
||||
/**
|
||||
* Public
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Shows this Toast
|
||||
*
|
||||
* @param {string} text Text to show
|
||||
* @param {real} duration Duration to show in milliseconds, defaults to 3000
|
||||
*/
|
||||
function show(text, duration=3000) {
|
||||
message.text = text;
|
||||
if (typeof duration !== "undefined") { // checks if parameter was passed
|
||||
time = Math.max(duration, 2 * fadeTime);
|
||||
}
|
||||
else {
|
||||
time = defaultTime;
|
||||
}
|
||||
animation.start();
|
||||
}
|
||||
|
||||
property bool selfDestroying: false // whether this Toast will self-destroy when it is finished
|
||||
|
||||
/**
|
||||
* Private
|
||||
*/
|
||||
|
||||
id: root
|
||||
|
||||
readonly property real defaultTime: 3000
|
||||
property real time: defaultTime
|
||||
readonly property real fadeTime: 300
|
||||
|
||||
property real margin: 10
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
margins: margin
|
||||
}
|
||||
|
||||
height: message.height + margin
|
||||
radius: margin
|
||||
|
||||
opacity: 0
|
||||
color: "#222222"
|
||||
|
||||
Text {
|
||||
id: message
|
||||
color: "white"
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
margins: margin / 2
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
id: animation
|
||||
running: false
|
||||
|
||||
|
||||
NumberAnimation {
|
||||
to: .9
|
||||
duration: fadeTime
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: time - 2 * fadeTime
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0
|
||||
duration: fadeTime
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && selfDestroying) {
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
gpt4all-chat/qml/ToastManager.qml
Normal file
60
gpt4all-chat/qml/ToastManager.qml
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Source: https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129
|
||||
* Adapted from StackOverflow: http://stackoverflow.com/questions/26879266/make-toast-in-android-by-qml
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
|
||||
/**
|
||||
* @brief Manager that creates Toasts dynamically
|
||||
*/
|
||||
ListView {
|
||||
/**
|
||||
* Public
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Shows a Toast
|
||||
*
|
||||
* @param {string} text Text to show
|
||||
* @param {real} duration Duration to show in milliseconds, defaults to 3000
|
||||
*/
|
||||
function show(text, duration=3000) {
|
||||
model.insert(0, {text: text, duration: duration});
|
||||
}
|
||||
|
||||
/**
|
||||
* Private
|
||||
*/
|
||||
|
||||
id: root
|
||||
|
||||
z: Infinity
|
||||
spacing: 5
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: 10
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
|
||||
interactive: false
|
||||
|
||||
displaced: Transition {
|
||||
NumberAnimation {
|
||||
properties: "y"
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Toast {
|
||||
Component.onCompleted: {
|
||||
if (typeof duration === "undefined") {
|
||||
show(text);
|
||||
}
|
||||
else {
|
||||
show(text, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
model: ListModel {id: model}
|
||||
}
|
Loading…
Reference in New Issue
Block a user