mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2025-06-23 05:58:48 +00: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/SwitchModelDialog.qml
|
||||||
qml/Theme.qml
|
qml/Theme.qml
|
||||||
qml/ThumbsDownDialog.qml
|
qml/ThumbsDownDialog.qml
|
||||||
|
qml/Toast.qml
|
||||||
|
qml/ToastManager.qml
|
||||||
qml/MyBusyIndicator.qml
|
qml/MyBusyIndicator.qml
|
||||||
qml/MyButton.qml
|
qml/MyButton.qml
|
||||||
qml/MyCheckBox.qml
|
qml/MyCheckBox.qml
|
||||||
|
@ -201,6 +201,11 @@ void ChatAPIWorker::request(const QString &apiKey,
|
|||||||
QNetworkRequest request(apiUrl);
|
QNetworkRequest request(apiUrl);
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
request.setRawHeader("Authorization", authorization.toUtf8());
|
request.setRawHeader("Authorization", authorization.toUtf8());
|
||||||
|
#if defined(DEBUG)
|
||||||
|
qDebug() << "ChatAPI::request"
|
||||||
|
<< "API URL: " << apiUrl.toString()
|
||||||
|
<< "Authorization: " << authorization.toUtf8();
|
||||||
|
#endif
|
||||||
m_networkManager = new QNetworkAccessManager(this);
|
m_networkManager = new QNetworkAccessManager(this);
|
||||||
QNetworkReply *reply = m_networkManager->post(request, array);
|
QNetworkReply *reply = m_networkManager->post(request, array);
|
||||||
connect(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
|
connect(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
|
||||||
@ -218,10 +223,28 @@ void ChatAPIWorker::handleFinished()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
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;
|
bool ok;
|
||||||
int code = response.toInt(&ok);
|
int code = response.toInt(&ok);
|
||||||
if (!ok || code != 200) {
|
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:"
|
qWarning().noquote() << "ERROR: ChatAPIWorker::handleFinished got HTTP Error" << code << "response:"
|
||||||
<< reply->errorString();
|
<< reply->errorString();
|
||||||
}
|
}
|
||||||
@ -238,7 +261,10 @@ void ChatAPIWorker::handleReadyRead()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
QVariant response = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||||
Q_ASSERT(response.isValid());
|
|
||||||
|
if (!response.isValid())
|
||||||
|
return;
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
int code = response.toInt(&ok);
|
int code = response.toInt(&ok);
|
||||||
if (!ok || code != 200) {
|
if (!ok || code != 200) {
|
||||||
|
@ -322,6 +322,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
|||||||
QVariantMap modelLoadProps;
|
QVariantMap modelLoadProps;
|
||||||
if (modelInfo.isOnline) {
|
if (modelInfo.isOnline) {
|
||||||
QString apiKey;
|
QString apiKey;
|
||||||
|
QString requestUrl;
|
||||||
QString modelName;
|
QString modelName;
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
@ -332,11 +333,24 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
|||||||
QJsonObject obj = doc.object();
|
QJsonObject obj = doc.object();
|
||||||
apiKey = obj["apiKey"].toString();
|
apiKey = obj["apiKey"].toString();
|
||||||
modelName = obj["modelName"].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_;
|
m_llModelType = LLModelType::API_;
|
||||||
ChatAPI *model = new ChatAPI();
|
ChatAPI *model = new ChatAPI();
|
||||||
model->setModelName(modelName);
|
model->setModelName(modelName);
|
||||||
model->setRequestURL(modelInfo.url());
|
model->setRequestURL(requestUrl);
|
||||||
model->setAPIKey(apiKey);
|
model->setAPIKey(apiKey);
|
||||||
m_llModelInfo.resetModel(this, model);
|
m_llModelInfo.resetModel(this, model);
|
||||||
} else if (!loadNewModel(modelInfo, modelLoadProps)) {
|
} else if (!loadNewModel(modelInfo, modelLoadProps)) {
|
||||||
|
@ -237,6 +237,54 @@ void Download::installModel(const QString &modelFile, const QString &apiKey)
|
|||||||
stream << doc.toJson();
|
stream << doc.toJson();
|
||||||
file.close();
|
file.close();
|
||||||
ModelList::globalInstance()->updateModelsFromDirectory();
|
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 }});
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
|
||||||
@ -255,11 +303,12 @@ void Download::removeModel(const QString &modelFile)
|
|||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
|
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
|
||||||
MySettings::globalInstance()->eraseModel(info);
|
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)
|
if (shouldRemoveInstalled)
|
||||||
ModelList::globalInstance()->removeInstalled(info);
|
ModelList::globalInstance()->removeInstalled(info);
|
||||||
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
|
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
|
||||||
file.remove();
|
file.remove();
|
||||||
|
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldRemoveInstalled) {
|
if (!shouldRemoveInstalled) {
|
||||||
|
@ -63,6 +63,7 @@ public:
|
|||||||
Q_INVOKABLE void downloadModel(const QString &modelFile);
|
Q_INVOKABLE void downloadModel(const QString &modelFile);
|
||||||
Q_INVOKABLE void cancelDownload(const QString &modelFile);
|
Q_INVOKABLE void cancelDownload(const QString &modelFile);
|
||||||
Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey);
|
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 void removeModel(const QString &modelFile);
|
||||||
Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const;
|
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,
|
void requestHashAndSave(const QString &hash, QCryptographicHash::Algorithm a, const QString &saveFilePath,
|
||||||
QFile *tempFile, QNetworkReply *modelReply);
|
QFile *tempFile, QNetworkReply *modelReply);
|
||||||
void latestNewsChanged();
|
void latestNewsChanged();
|
||||||
|
void toastMessage(const QString &message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void parseReleaseJsonFile(const QByteArray &jsonData);
|
void parseReleaseJsonFile(const QByteArray &jsonData);
|
||||||
|
@ -514,6 +514,17 @@ ModelList::ModelList()
|
|||||||
QCoreApplication::instance()->installEventFilter(this);
|
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)
|
bool ModelList::eventFilter(QObject *obj, QEvent *ev)
|
||||||
{
|
{
|
||||||
if (obj == QCoreApplication::instance() && ev->type() == QEvent::LanguageChange)
|
if (obj == QCoreApplication::instance() && ev->type() == QEvent::LanguageChange)
|
||||||
@ -703,6 +714,8 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
|
|||||||
return info->isDefault;
|
return info->isDefault;
|
||||||
case OnlineRole:
|
case OnlineRole:
|
||||||
return info->isOnline;
|
return info->isOnline;
|
||||||
|
case CompatibleApiRole:
|
||||||
|
return info->isCompatibleApi;
|
||||||
case DescriptionRole:
|
case DescriptionRole:
|
||||||
return info->description();
|
return info->description();
|
||||||
case RequiresVersionRole:
|
case RequiresVersionRole:
|
||||||
@ -859,6 +872,8 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
|
|||||||
info->isDefault = value.toBool(); break;
|
info->isDefault = value.toBool(); break;
|
||||||
case OnlineRole:
|
case OnlineRole:
|
||||||
info->isOnline = value.toBool(); break;
|
info->isOnline = value.toBool(); break;
|
||||||
|
case CompatibleApiRole:
|
||||||
|
info->isCompatibleApi = value.toBool(); break;
|
||||||
case DescriptionRole:
|
case DescriptionRole:
|
||||||
info->setDescription(value.toString()); break;
|
info->setDescription(value.toString()); break;
|
||||||
case RequiresVersionRole:
|
case RequiresVersionRole:
|
||||||
@ -1079,6 +1094,7 @@ QString ModelList::clone(const ModelInfo &model)
|
|||||||
{ ModelList::FilenameRole, model.filename() },
|
{ ModelList::FilenameRole, model.filename() },
|
||||||
{ ModelList::DirpathRole, model.dirpath },
|
{ ModelList::DirpathRole, model.dirpath },
|
||||||
{ ModelList::OnlineRole, model.isOnline },
|
{ ModelList::OnlineRole, model.isOnline },
|
||||||
|
{ ModelList::CompatibleApiRole, model.isCompatibleApi },
|
||||||
{ ModelList::IsEmbeddingModelRole, model.isEmbeddingModel },
|
{ ModelList::IsEmbeddingModelRole, model.isEmbeddingModel },
|
||||||
{ ModelList::TemperatureRole, model.temperature() },
|
{ ModelList::TemperatureRole, model.temperature() },
|
||||||
{ ModelList::TopPRole, model.topP() },
|
{ ModelList::TopPRole, model.topP() },
|
||||||
@ -1113,7 +1129,7 @@ void ModelList::removeInstalled(const ModelInfo &model)
|
|||||||
{
|
{
|
||||||
Q_ASSERT(model.installed);
|
Q_ASSERT(model.installed);
|
||||||
Q_ASSERT(!model.isClone());
|
Q_ASSERT(!model.isClone());
|
||||||
Q_ASSERT(model.isDiscovered() || model.description() == "" /*indicates sideloaded*/);
|
Q_ASSERT(model.isDiscovered() || model.isCompatibleApi || model.description() == "" /*indicates sideloaded*/);
|
||||||
removeInternal(model);
|
removeInternal(model);
|
||||||
emit layoutChanged();
|
emit layoutChanged();
|
||||||
}
|
}
|
||||||
@ -1260,14 +1276,53 @@ void ModelList::updateModelsFromDirectory()
|
|||||||
|
|
||||||
QFileInfo info = it.fileInfo();
|
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) {
|
for (const QString &id : modelsById) {
|
||||||
QVector<QPair<int, QVariant>> data {
|
QVector<QPair<int, QVariant>> data {
|
||||||
{ InstalledRole, true },
|
{ InstalledRole, true },
|
||||||
{ FilenameRole, filename },
|
{ FilenameRole, filename },
|
||||||
{ OnlineRole, filename.endsWith(".rmodel") },
|
{ OnlineRole, isOnline },
|
||||||
|
{ CompatibleApiRole, isCompatibleApi },
|
||||||
{ DirpathRole, info.dir().absolutePath() + "/" },
|
{ DirpathRole, info.dir().absolutePath() + "/" },
|
||||||
{ FilesizeRole, toFileSize(info.size()) },
|
{ 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);
|
updateData(id, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1657,6 +1712,34 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
|
|||||||
};
|
};
|
||||||
updateData(id, data);
|
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)
|
void ModelList::updateDiscoveredInstalled(const ModelInfo &info)
|
||||||
|
@ -35,6 +35,7 @@ struct ModelInfo {
|
|||||||
Q_PROPERTY(bool installed MEMBER installed)
|
Q_PROPERTY(bool installed MEMBER installed)
|
||||||
Q_PROPERTY(bool isDefault MEMBER isDefault)
|
Q_PROPERTY(bool isDefault MEMBER isDefault)
|
||||||
Q_PROPERTY(bool isOnline MEMBER isOnline)
|
Q_PROPERTY(bool isOnline MEMBER isOnline)
|
||||||
|
Q_PROPERTY(bool isCompatibleApi MEMBER isCompatibleApi)
|
||||||
Q_PROPERTY(QString description READ description WRITE setDescription)
|
Q_PROPERTY(QString description READ description WRITE setDescription)
|
||||||
Q_PROPERTY(QString requiresVersion MEMBER requiresVersion)
|
Q_PROPERTY(QString requiresVersion MEMBER requiresVersion)
|
||||||
Q_PROPERTY(QString versionRemoved MEMBER versionRemoved)
|
Q_PROPERTY(QString versionRemoved MEMBER versionRemoved)
|
||||||
@ -123,7 +124,17 @@ public:
|
|||||||
bool calcHash = false;
|
bool calcHash = false;
|
||||||
bool installed = false;
|
bool installed = false;
|
||||||
bool isDefault = 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;
|
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 requiresVersion;
|
||||||
QString versionRemoved;
|
QString versionRemoved;
|
||||||
qint64 bytesReceived = 0;
|
qint64 bytesReceived = 0;
|
||||||
@ -276,6 +287,9 @@ class ModelList : public QAbstractListModel
|
|||||||
public:
|
public:
|
||||||
static ModelList *globalInstance();
|
static ModelList *globalInstance();
|
||||||
|
|
||||||
|
static QString compatibleModelNameHash(QUrl baseUrl, QString modelName);
|
||||||
|
static QString compatibleModelFilename(QUrl baseUrl, QString modelName);
|
||||||
|
|
||||||
enum DiscoverSort {
|
enum DiscoverSort {
|
||||||
Default,
|
Default,
|
||||||
Likes,
|
Likes,
|
||||||
@ -295,6 +309,7 @@ public:
|
|||||||
InstalledRole,
|
InstalledRole,
|
||||||
DefaultRole,
|
DefaultRole,
|
||||||
OnlineRole,
|
OnlineRole,
|
||||||
|
CompatibleApiRole,
|
||||||
DescriptionRole,
|
DescriptionRole,
|
||||||
RequiresVersionRole,
|
RequiresVersionRole,
|
||||||
VersionRemovedRole,
|
VersionRemovedRole,
|
||||||
@ -347,6 +362,7 @@ public:
|
|||||||
roles[InstalledRole] = "installed";
|
roles[InstalledRole] = "installed";
|
||||||
roles[DefaultRole] = "isDefault";
|
roles[DefaultRole] = "isDefault";
|
||||||
roles[OnlineRole] = "isOnline";
|
roles[OnlineRole] = "isOnline";
|
||||||
|
roles[CompatibleApiRole] = "isCompatibleApi";
|
||||||
roles[DescriptionRole] = "description";
|
roles[DescriptionRole] = "description";
|
||||||
roles[RequiresVersionRole] = "requiresVersion";
|
roles[RequiresVersionRole] = "requiresVersion";
|
||||||
roles[VersionRemovedRole] = "versionRemoved";
|
roles[VersionRemovedRole] = "versionRemoved";
|
||||||
|
@ -99,6 +99,15 @@ Network *Network::globalInstance()
|
|||||||
return networkInstance();
|
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()
|
Network::Network()
|
||||||
: QObject{nullptr}
|
: QObject{nullptr}
|
||||||
{
|
{
|
||||||
|
@ -23,6 +23,7 @@ class Network : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
static Network *globalInstance();
|
static Network *globalInstance();
|
||||||
|
static bool isHttpUrlValid(const QUrl url);
|
||||||
|
|
||||||
Q_INVOKABLE QString generateUniqueId() const;
|
Q_INVOKABLE QString generateUniqueId() const;
|
||||||
Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation);
|
Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation);
|
||||||
|
@ -25,6 +25,10 @@ Rectangle {
|
|||||||
color: theme.viewBackground
|
color: theme.viewBackground
|
||||||
signal modelsViewRequested()
|
signal modelsViewRequested()
|
||||||
|
|
||||||
|
ToastManager {
|
||||||
|
id: messageToast
|
||||||
|
}
|
||||||
|
|
||||||
PopupDialog {
|
PopupDialog {
|
||||||
id: downloadingErrorPopup
|
id: downloadingErrorPopup
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@ -437,10 +441,35 @@ Rectangle {
|
|||||||
text: qsTr("Install")
|
text: qsTr("Install")
|
||||||
font.pixelSize: theme.fontSizeLarge
|
font.pixelSize: theme.fontSizeLarge
|
||||||
onClicked: {
|
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();
|
apiKey.showError();
|
||||||
|
if (!baseUrlOk)
|
||||||
|
baseUrl.showError();
|
||||||
|
if (!modelNameOk)
|
||||||
|
modelName.showError();
|
||||||
|
|
||||||
|
if (!apiKeyOk || !baseUrlOk || !modelNameOk)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isCompatibleApi)
|
||||||
|
Download.installModel(
|
||||||
|
filename,
|
||||||
|
apiKeyText,
|
||||||
|
);
|
||||||
else
|
else
|
||||||
Download.installModel(filename, apiKey.text);
|
Download.installCompatibleModel(
|
||||||
|
modelNameText,
|
||||||
|
apiKeyText,
|
||||||
|
baseUrlText,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Accessible.role: Accessible.Button
|
Accessible.role: Accessible.Button
|
||||||
Accessible.name: qsTr("Install")
|
Accessible.name: qsTr("Install")
|
||||||
@ -571,16 +600,59 @@ Rectangle {
|
|||||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
function showError() {
|
function showError() {
|
||||||
apiKey.placeholderTextColor = theme.textErrorColor
|
messageToast.show(qsTr("ERROR: $API_KEY is empty."));
|
||||||
|
apiKey.placeholderTextColor = theme.textErrorColor;
|
||||||
}
|
}
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
apiKey.placeholderTextColor = theme.mutedTextColor
|
apiKey.placeholderTextColor = theme.mutedTextColor;
|
||||||
}
|
}
|
||||||
placeholderText: qsTr("enter $API_KEY")
|
placeholderText: qsTr("enter $API_KEY")
|
||||||
Accessible.role: Accessible.EditableText
|
Accessible.role: Accessible.EditableText
|
||||||
Accessible.name: placeholderText
|
Accessible.name: placeholderText
|
||||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
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
|
rightPadding: 60
|
||||||
leftPadding: 60
|
leftPadding: 60
|
||||||
property string defaultModel: ""
|
property string defaultModel: ""
|
||||||
|
property string defaultModelName: ""
|
||||||
function updateDefaultModel() {
|
function updateDefaultModel() {
|
||||||
var i = comboBox.find(MySettings.userDefaultModel)
|
var i = comboBox.find(MySettings.userDefaultModel)
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
@ -703,9 +704,14 @@ Rectangle {
|
|||||||
} else {
|
} else {
|
||||||
defaultModel = comboBox.valueAt(0);
|
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: {
|
onClicked: {
|
||||||
var i = comboBox.find(MySettings.userDefaultModel)
|
var i = comboBox.find(MySettings.userDefaultModel)
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
|
@ -17,6 +17,10 @@ Rectangle {
|
|||||||
|
|
||||||
signal addModelViewRequested()
|
signal addModelViewRequested()
|
||||||
|
|
||||||
|
ToastManager {
|
||||||
|
id: messageToast
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 20
|
anchors.margins: 20
|
||||||
@ -233,10 +237,35 @@ Rectangle {
|
|||||||
text: qsTr("Install")
|
text: qsTr("Install")
|
||||||
font.pixelSize: theme.fontSizeLarge
|
font.pixelSize: theme.fontSizeLarge
|
||||||
onClicked: {
|
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();
|
apiKey.showError();
|
||||||
|
if (!baseUrlOk)
|
||||||
|
baseUrl.showError();
|
||||||
|
if (!modelNameOk)
|
||||||
|
modelName.showError();
|
||||||
|
|
||||||
|
if (!apiKeyOk || !baseUrlOk || !modelNameOk)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!isCompatibleApi)
|
||||||
|
Download.installModel(
|
||||||
|
filename,
|
||||||
|
apiKeyText,
|
||||||
|
);
|
||||||
else
|
else
|
||||||
Download.installModel(filename, apiKey.text);
|
Download.installCompatibleModel(
|
||||||
|
modelNameText,
|
||||||
|
apiKeyText,
|
||||||
|
baseUrlText,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Accessible.role: Accessible.Button
|
Accessible.role: Accessible.Button
|
||||||
Accessible.name: qsTr("Install")
|
Accessible.name: qsTr("Install")
|
||||||
@ -367,16 +396,59 @@ Rectangle {
|
|||||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
function showError() {
|
function showError() {
|
||||||
apiKey.placeholderTextColor = theme.textErrorColor
|
messageToast.show(qsTr("ERROR: $API_KEY is empty."));
|
||||||
|
apiKey.placeholderTextColor = theme.textErrorColor;
|
||||||
}
|
}
|
||||||
onTextChanged: {
|
onTextChanged: {
|
||||||
apiKey.placeholderTextColor = theme.mutedTextColor
|
apiKey.placeholderTextColor = theme.mutedTextColor;
|
||||||
}
|
}
|
||||||
placeholderText: qsTr("enter $API_KEY")
|
placeholderText: qsTr("enter $API_KEY")
|
||||||
Accessible.role: Accessible.EditableText
|
Accessible.role: Accessible.EditableText
|
||||||
Accessible.name: placeholderText
|
Accessible.name: placeholderText
|
||||||
Accessible.description: qsTr("Whether the file hash is being calculated")
|
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