From 83c76be68a600f8c65d4eed53c0cd7497697559d Mon Sep 17 00:00:00 2001 From: Adam Treat Date: Tue, 5 Mar 2024 11:31:31 -0500 Subject: [PATCH] Model discovery. Signed-off-by: Adam Treat --- gpt4all-chat/download.cpp | 32 +- gpt4all-chat/download.h | 4 +- gpt4all-chat/modellist.cpp | 976 +++++++++++++++++---- gpt4all-chat/modellist.h | 141 ++- gpt4all-chat/mysettings.cpp | 210 ++++- gpt4all-chat/mysettings.h | 21 +- gpt4all-chat/qml/ModelDownloaderDialog.qml | 236 ++++- gpt4all-chat/qml/ModelSettings.qml | 4 +- gpt4all-chat/qml/MyMiniButton.qml | 4 +- 9 files changed, 1362 insertions(+), 266 deletions(-) diff --git a/gpt4all-chat/download.cpp b/gpt4all-chat/download.cpp index e1a688a4..6eafc512 100644 --- a/gpt4all-chat/download.cpp +++ b/gpt4all-chat/download.cpp @@ -130,7 +130,7 @@ void Download::downloadModel(const QString &modelFile) ModelList::globalInstance()->updateDataByFilename(modelFile, ModelList::DownloadingRole, true); ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile); - QString url = !info.url.isEmpty() ? info.url : "http://gpt4all.io/models/gguf/" + modelFile; + QString url = !info.url().isEmpty() ? info.url() : "http://gpt4all.io/models/gguf/" + modelFile; Network::globalInstance()->sendDownloadStarted(modelFile); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::User, modelFile); @@ -201,6 +201,8 @@ void Download::removeModel(const QString &modelFile) QFile file(filePath); if (file.exists()) { + const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile); + ModelList::globalInstance()->removeInstalled(info); Network::globalInstance()->sendRemoveModel(modelFile); file.remove(); } @@ -364,8 +366,8 @@ HashAndSaveFile::HashAndSaveFile() m_hashAndSaveThread.start(); } -void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &saveFilePath, - QFile *tempFile, QNetworkReply *modelReply) +void HashAndSaveFile::hashAndSave(const QString &expectedHash, QCryptographicHash::Algorithm a, + const QString &saveFilePath, QFile *tempFile, QNetworkReply *modelReply) { Q_ASSERT(!tempFile->isOpen()); QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString(); @@ -379,13 +381,16 @@ void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &sa return; } - QCryptographicHash hash(QCryptographicHash::Md5); + QCryptographicHash hash(a); while(!tempFile->atEnd()) hash.addData(tempFile->read(16384)); - if (hash.result().toHex() != expectedHash) { + if (hash.result().toHex() != expectedHash.toLatin1()) { tempFile->close(); const QString error - = QString("ERROR: Download error MD5SUM did not match: %1 != %2 for %3").arg(hash.result().toHex()).arg(expectedHash).arg(modelFilename); + = QString("ERROR: Download error hash did not match: %1 != %2 for %3") + .arg(hash.result().toHex()) + .arg(expectedHash.toLatin1()) + .arg(modelFilename); qWarning() << error; tempFile->remove(); emit hashAndSaveFinished(false, error, tempFile, modelReply); @@ -472,9 +477,12 @@ void Download::handleModelDownloadFinished() // Notify that we are calculating hash ModelList::globalInstance()->updateDataByFilename(modelFilename, ModelList::CalcHashRole, true); - QByteArray md5sum = ModelList::globalInstance()->modelInfoByFilename(modelFilename).md5sum; + QByteArray hash = ModelList::globalInstance()->modelInfoByFilename(modelFilename).hash; + ModelInfo::HashAlgorithm hashAlgorithm = ModelList::globalInstance()->modelInfoByFilename(modelFilename).hashAlgorithm; const QString saveFilePath = MySettings::globalInstance()->modelPath() + modelFilename; - emit requestHashAndSave(md5sum, saveFilePath, tempFile, modelReply); + emit requestHashAndSave(hash, + (hashAlgorithm == ModelInfo::Md5 ? QCryptographicHash::Md5 : QCryptographicHash::Sha256), + saveFilePath, tempFile, modelReply); } void Download::handleHashAndSaveFinished(bool success, const QString &error, @@ -489,10 +497,14 @@ void Download::handleHashAndSaveFinished(bool success, const QString &error, tempFile->deleteLater(); ModelList::globalInstance()->updateDataByFilename(modelFilename, ModelList::DownloadingRole, false); - if (!success) + if (!success) { ModelList::globalInstance()->updateDataByFilename(modelFilename, ModelList::DownloadErrorRole, error); - else + } else { + ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFilename); + if (info.isDiscovered()) + ModelList::globalInstance()->updateDiscoveredInstalled(info); ModelList::globalInstance()->updateDataByFilename(modelFilename, ModelList::DownloadErrorRole, QString()); + } } void Download::handleReadyRead() diff --git a/gpt4all-chat/download.h b/gpt4all-chat/download.h index 9a87687b..2a4778ae 100644 --- a/gpt4all-chat/download.h +++ b/gpt4all-chat/download.h @@ -28,7 +28,7 @@ public: HashAndSaveFile(); public Q_SLOTS: - void hashAndSave(const QString &hash, const QString &saveFilePath, + void hashAndSave(const QString &hash, QCryptographicHash::Algorithm a, const QString &saveFilePath, QFile *tempFile, QNetworkReply *modelReply); Q_SIGNALS: @@ -72,7 +72,7 @@ private Q_SLOTS: Q_SIGNALS: void releaseInfoChanged(); void hasNewerReleaseChanged(); - void requestHashAndSave(const QString &hash, const QString &saveFilePath, + void requestHashAndSave(const QString &hash, QCryptographicHash::Algorithm a, const QString &saveFilePath, QFile *tempFile, QNetworkReply *modelReply); private: diff --git a/gpt4all-chat/modellist.cpp b/gpt4all-chat/modellist.cpp index 3b4d284d..12268b50 100644 --- a/gpt4all-chat/modellist.cpp +++ b/gpt4all-chat/modellist.cpp @@ -29,7 +29,7 @@ QString ModelInfo::name() const void ModelInfo::setName(const QString &name) { - if (isClone) MySettings::globalInstance()->setModelName(*this, name, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelName(*this, name, true /*force*/); m_name = name; } @@ -40,10 +40,109 @@ QString ModelInfo::filename() const void ModelInfo::setFilename(const QString &filename) { - if (isClone) MySettings::globalInstance()->setModelFilename(*this, filename, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelFilename(*this, filename, true /*force*/); m_filename = filename; } +QString ModelInfo::description() const +{ + return MySettings::globalInstance()->modelDescription(*this); +} + +void ModelInfo::setDescription(const QString &d) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelDescription(*this, d, true /*force*/); + m_description = d; +} + +QString ModelInfo::url() const +{ + return MySettings::globalInstance()->modelUrl(*this); +} + +void ModelInfo::setUrl(const QString &u) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelUrl(*this, u, true /*force*/); + m_url = u; +} + +QString ModelInfo::quant() const +{ + return MySettings::globalInstance()->modelQuant(*this); +} + +void ModelInfo::setQuant(const QString &q) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelQuant(*this, q, true /*force*/); + m_quant = q; +} + +QString ModelInfo::type() const +{ + return MySettings::globalInstance()->modelType(*this); +} + +void ModelInfo::setType(const QString &t) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelType(*this, t, true /*force*/); + m_type = t; +} + +bool ModelInfo::isClone() const +{ + return MySettings::globalInstance()->modelIsClone(*this); +} + +void ModelInfo::setIsClone(bool b) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelIsClone(*this, b, true /*force*/); + m_isClone = b; +} + +bool ModelInfo::isDiscovered() const +{ + return MySettings::globalInstance()->modelIsDiscovered(*this); +} + +void ModelInfo::setIsDiscovered(bool b) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelIsDiscovered(*this, b, true /*force*/); + m_isDiscovered = b; +} + +int ModelInfo::likes() const +{ + return MySettings::globalInstance()->modelLikes(*this); +} + +void ModelInfo::setLikes(int l) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelLikes(*this, l, true /*force*/); + m_likes = l; +} + +int ModelInfo::downloads() const +{ + return MySettings::globalInstance()->modelDownloads(*this); +} + +void ModelInfo::setDownloads(int d) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelDownloads(*this, d, true /*force*/); + m_downloads = d; +} + +QDateTime ModelInfo::recency() const +{ + return MySettings::globalInstance()->modelRecency(*this); +} + +void ModelInfo::setRecency(const QDateTime &r) +{ + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelRecency(*this, r, true /*force*/); + m_recency = r; +} + double ModelInfo::temperature() const { return MySettings::globalInstance()->modelTemperature(*this); @@ -51,7 +150,7 @@ double ModelInfo::temperature() const void ModelInfo::setTemperature(double t) { - if (isClone) MySettings::globalInstance()->setModelTemperature(*this, t, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelTemperature(*this, t, true /*force*/); m_temperature = t; } @@ -67,13 +166,13 @@ double ModelInfo::minP() const void ModelInfo::setTopP(double p) { - if (isClone) MySettings::globalInstance()->setModelTopP(*this, p, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelTopP(*this, p, true /*force*/); m_topP = p; } void ModelInfo::setMinP(double p) { - if (isClone) MySettings::globalInstance()->setModelMinP(*this, p, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelMinP(*this, p, true /*force*/); m_minP = p; } @@ -84,7 +183,7 @@ int ModelInfo::topK() const void ModelInfo::setTopK(int k) { - if (isClone) MySettings::globalInstance()->setModelTopK(*this, k, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelTopK(*this, k, true /*force*/); m_topK = k; } @@ -95,7 +194,7 @@ int ModelInfo::maxLength() const void ModelInfo::setMaxLength(int l) { - if (isClone) MySettings::globalInstance()->setModelMaxLength(*this, l, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelMaxLength(*this, l, true /*force*/); m_maxLength = l; } @@ -106,7 +205,7 @@ int ModelInfo::promptBatchSize() const void ModelInfo::setPromptBatchSize(int s) { - if (isClone) MySettings::globalInstance()->setModelPromptBatchSize(*this, s, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelPromptBatchSize(*this, s, true /*force*/); m_promptBatchSize = s; } @@ -117,7 +216,7 @@ int ModelInfo::contextLength() const void ModelInfo::setContextLength(int l) { - if (isClone) MySettings::globalInstance()->setModelContextLength(*this, l, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelContextLength(*this, l, true /*force*/); m_contextLength = l; } @@ -140,7 +239,7 @@ int ModelInfo::gpuLayers() const void ModelInfo::setGpuLayers(int l) { - if (isClone) MySettings::globalInstance()->setModelGpuLayers(*this, l, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelGpuLayers(*this, l, true /*force*/); m_gpuLayers = l; } @@ -164,7 +263,7 @@ double ModelInfo::repeatPenalty() const void ModelInfo::setRepeatPenalty(double p) { - if (isClone) MySettings::globalInstance()->setModelRepeatPenalty(*this, p, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelRepeatPenalty(*this, p, true /*force*/); m_repeatPenalty = p; } @@ -175,7 +274,7 @@ int ModelInfo::repeatPenaltyTokens() const void ModelInfo::setRepeatPenaltyTokens(int t) { - if (isClone) MySettings::globalInstance()->setModelRepeatPenaltyTokens(*this, t, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelRepeatPenaltyTokens(*this, t, true /*force*/); m_repeatPenaltyTokens = t; } @@ -186,7 +285,7 @@ QString ModelInfo::promptTemplate() const void ModelInfo::setPromptTemplate(const QString &t) { - if (isClone) MySettings::globalInstance()->setModelPromptTemplate(*this, t, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelPromptTemplate(*this, t, true /*force*/); m_promptTemplate = t; } @@ -197,10 +296,15 @@ QString ModelInfo::systemPrompt() const void ModelInfo::setSystemPrompt(const QString &p) { - if (isClone) MySettings::globalInstance()->setModelSystemPrompt(*this, p, isClone /*force*/); + if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPrompt(*this, p, true /*force*/); m_systemPrompt = p; } +bool ModelInfo::shouldSaveMetadata() const +{ + return installed && (isClone() || isDiscovered() || description() == "" /*indicates sideloaded*/); +} + EmbeddingModels::EmbeddingModels(QObject *parent) : QSortFilterProxyModel(parent) { @@ -286,7 +390,9 @@ bool DownloadableModels::filterAcceptsRow(int sourceRow, bool withinLimit = sourceRow < (m_expanded ? sourceModel()->rowCount() : m_limit); QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); bool isDownloadable = !sourceModel()->data(index, ModelList::DescriptionRole).toString().isEmpty(); - return withinLimit && isDownloadable; + bool isInstalled = !sourceModel()->data(index, ModelList::InstalledRole).toString().isEmpty(); + bool isIncomplete = !sourceModel()->data(index, ModelList::IncompleteRole).toString().isEmpty(); + return withinLimit && (isDownloadable || isInstalled || isIncomplete); } int DownloadableModels::count() const @@ -308,6 +414,13 @@ void DownloadableModels::setExpanded(bool expanded) } } +void DownloadableModels::discoverAndFilter(const QString &discover) +{ + m_discoverFilter = discover; + ModelList *ml = qobject_cast(parent()); + ml->discoverSearch(discover); +} + class MyModelList: public ModelList { }; Q_GLOBAL_STATIC(MyModelList, modelListInstance) ModelList *ModelList::globalInstance() @@ -321,6 +434,12 @@ ModelList::ModelList() , m_installedModels(new InstalledModels(this)) , m_downloadableModels(new DownloadableModels(this)) , m_asyncModelRequestOngoing(false) + , m_discoverLimit(20) + , m_discoverSortDirection(-1) + , m_discoverSort(Likes) + , m_discoverNumberOfResults(0) + , m_discoverResultsCompleted(0) + , m_discoverInProgress(false) { m_embeddingModels->setSourceModel(this); m_installedModels->setSourceModel(this); @@ -447,11 +566,26 @@ bool ModelList::containsByFilename(const QString &filename) const return false; } -bool ModelList::lessThan(const ModelInfo* a, const ModelInfo* b) +bool ModelList::lessThan(const ModelInfo* a, const ModelInfo* b, DiscoverSort s, int d) { + // Rule -1a: Discover sort + if (a->isDiscovered() && b->isDiscovered()) { + switch (s) { + case Default: break; + case Likes: return (d > 0 ? a->likes() < b->likes() : a->likes() > b->likes()); + case Downloads: return (d > 0 ? a->downloads() < b->downloads() : a->downloads() > b->downloads()); + case Recent: return (d > 0 ? a->recency() < b->recency() : a->recency() > b->recency()); + } + } + + // Rule -1: Discovered before non-discovered + if (a->isDiscovered() != b->isDiscovered()) { + return a->isDiscovered(); + } + // Rule 0: Non-clone before clone - if (a->isClone != b->isClone) { - return !a->isClone; + if (a->isClone() != b->isClone()) { + return !a->isClone(); } // Rule 1: Non-empty 'order' before empty @@ -477,24 +611,30 @@ void ModelList::addModel(const QString &id) return; } - int modelSizeBefore = 0; - int modelSizeAfter = 0; - { - QMutexLocker locker(&m_mutex); - modelSizeBefore = m_models.size(); - } - beginInsertRows(QModelIndex(), modelSizeBefore, modelSizeBefore); - { - QMutexLocker locker(&m_mutex); - ModelInfo *info = new ModelInfo; - info->setId(id); - m_models.append(info); - m_modelMap.insert(id, info); - std::stable_sort(m_models.begin(), m_models.end(), ModelList::lessThan); - modelSizeAfter = m_models.size(); - } + ModelInfo *info = new ModelInfo; + info->setId(id); + + m_mutex.lock(); + auto s = m_discoverSort; + auto d = m_discoverSortDirection; + const auto insertPosition = std::lower_bound(m_models.begin(), m_models.end(), info, + [s, d](const ModelInfo* lhs, const ModelInfo* rhs) { + return ModelList::lessThan(lhs, rhs, s, d); + }); + const int index = std::distance(m_models.begin(), insertPosition); + m_mutex.unlock(); + + // NOTE: The begin/end rows cannot have a lock placed around them. We calculate the index ahead + // of time and this works because this class is designed carefully so that only one thread is + // responsible for insertion, deletion, and update + + beginInsertRows(QModelIndex(), index, index); + m_mutex.lock(); + m_models.insert(insertPosition, info); + m_modelMap.insert(id, info); + m_mutex.unlock(); endInsertRows(); - emit dataChanged(index(0, 0), index(modelSizeAfter - 1, 0)); + emit userDefaultModelListChanged(); } @@ -533,8 +673,10 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const return info->dirpath; case FilesizeRole: return info->filesize; - case Md5sumRole: - return info->md5sum; + case HashRole: + return info->hash; + case HashAlgorithmRole: + return info->hashAlgorithm; case CalcHashRole: return info->calcHash; case InstalledRole: @@ -546,13 +688,13 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const case DisableGUIRole: return info->disableGUI; case DescriptionRole: - return info->description; + return info->description(); case RequiresVersionRole: return info->requiresVersion; case DeprecatedVersionRole: return info->deprecatedVersion; case UrlRole: - return info->url; + return info->url(); case BytesReceivedRole: return info->bytesReceived; case BytesTotalRole: @@ -574,11 +716,13 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const case ParametersRole: return info->parameters; case QuantRole: - return info->quant; + return info->quant(); case TypeRole: - return info->type; + return info->type(); case IsCloneRole: - return info->isClone; + return info->isClone(); + case IsDiscoveredRole: + return info->isDiscovered(); case TemperatureRole: return info->temperature(); case TopPRole: @@ -603,6 +747,13 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const return info->promptTemplate(); case SystemPromptRole: return info->systemPrompt(); + case LikesRole: + return info->likes(); + case DownloadsRole: + return info->downloads(); + case RecencyRole: + return info->recency(); + } return QVariant(); @@ -635,9 +786,13 @@ QVariant ModelList::data(const QModelIndex &index, int role) const void ModelList::updateData(const QString &id, int role, const QVariant &value) { - int modelSize; - bool updateInstalled; - bool updateIncomplete; + QVector> data; + data.append(qMakePair(role, value)); + updateData(id, data); +} + +void ModelList::updateData(const QString &id, const QVector> &data) +{ int index; { QMutexLocker locker(&m_mutex); @@ -653,108 +808,133 @@ void ModelList::updateData(const QString &id, int role, const QVariant &value) return; } - switch (role) { - case IdRole: - info->setId(value.toString()); break; - case NameRole: - info->setName(value.toString()); break; - case FilenameRole: - info->setFilename(value.toString()); break; - case DirpathRole: - info->dirpath = value.toString(); break; - case FilesizeRole: - info->filesize = value.toString(); break; - case Md5sumRole: - info->md5sum = value.toByteArray(); break; - case CalcHashRole: - info->calcHash = value.toBool(); break; - case InstalledRole: - info->installed = value.toBool(); break; - case DefaultRole: - info->isDefault = value.toBool(); break; - case OnlineRole: - info->isOnline = value.toBool(); break; - case DisableGUIRole: - info->disableGUI = value.toBool(); break; - case DescriptionRole: - info->description = value.toString(); break; - case RequiresVersionRole: - info->requiresVersion = value.toString(); break; - case DeprecatedVersionRole: - info->deprecatedVersion = value.toString(); break; - case UrlRole: - info->url = value.toString(); break; - case BytesReceivedRole: - info->bytesReceived = value.toLongLong(); break; - case BytesTotalRole: - info->bytesTotal = value.toLongLong(); break; - case TimestampRole: - info->timestamp = value.toLongLong(); break; - case SpeedRole: - info->speed = value.toString(); break; - case DownloadingRole: - info->isDownloading = value.toBool(); break; - case IncompleteRole: - info->isIncomplete = value.toBool(); break; - case DownloadErrorRole: - info->downloadError = value.toString(); break; - case OrderRole: - info->order = value.toString(); break; - case RamrequiredRole: - info->ramrequired = value.toInt(); break; - case ParametersRole: - info->parameters = value.toString(); break; - case QuantRole: - info->quant = value.toString(); break; - case TypeRole: - info->type = value.toString(); break; - case IsCloneRole: - info->isClone = value.toBool(); break; - case TemperatureRole: - info->setTemperature(value.toDouble()); break; - case TopPRole: - info->setTopP(value.toDouble()); break; - case MinPRole: - info->setMinP(value.toDouble()); break; - case TopKRole: - info->setTopK(value.toInt()); break; - case MaxLengthRole: - info->setMaxLength(value.toInt()); break; - case PromptBatchSizeRole: - info->setPromptBatchSize(value.toInt()); break; - case ContextLengthRole: - info->setContextLength(value.toInt()); break; - case GpuLayersRole: - info->setGpuLayers(value.toInt()); break; - case RepeatPenaltyRole: - info->setRepeatPenalty(value.toDouble()); break; - case RepeatPenaltyTokensRole: - info->setRepeatPenaltyTokens(value.toInt()); break; - case PromptTemplateRole: - info->setPromptTemplate(value.toString()); break; - case SystemPromptRole: - info->setSystemPrompt(value.toString()); break; + for (const auto &d : data) { + const int role = d.first; + const QVariant value = d.second; + switch (role) { + case IdRole: + info->setId(value.toString()); break; + case NameRole: + info->setName(value.toString()); break; + case FilenameRole: + info->setFilename(value.toString()); break; + case DirpathRole: + info->dirpath = value.toString(); break; + case FilesizeRole: + info->filesize = value.toString(); break; + case HashRole: + info->hash = value.toByteArray(); break; + case HashAlgorithmRole: + info->hashAlgorithm = static_cast(value.toInt()); break; + case CalcHashRole: + info->calcHash = value.toBool(); break; + case InstalledRole: + info->installed = value.toBool(); break; + case DefaultRole: + info->isDefault = value.toBool(); break; + case OnlineRole: + info->isOnline = value.toBool(); break; + case DisableGUIRole: + info->disableGUI = value.toBool(); break; + case DescriptionRole: + info->setDescription(value.toString()); break; + case RequiresVersionRole: + info->requiresVersion = value.toString(); break; + case DeprecatedVersionRole: + info->deprecatedVersion = value.toString(); break; + case UrlRole: + info->setUrl(value.toString()); break; + case BytesReceivedRole: + info->bytesReceived = value.toLongLong(); break; + case BytesTotalRole: + info->bytesTotal = value.toLongLong(); break; + case TimestampRole: + info->timestamp = value.toLongLong(); break; + case SpeedRole: + info->speed = value.toString(); break; + case DownloadingRole: + info->isDownloading = value.toBool(); break; + case IncompleteRole: + info->isIncomplete = value.toBool(); break; + case DownloadErrorRole: + info->downloadError = value.toString(); break; + case OrderRole: + info->order = value.toString(); break; + case RamrequiredRole: + info->ramrequired = value.toInt(); break; + case ParametersRole: + info->parameters = value.toString(); break; + case QuantRole: + info->setQuant(value.toString()); break; + case TypeRole: + info->setType(value.toString()); break; + case IsCloneRole: + info->setIsClone(value.toBool()); break; + case IsDiscoveredRole: + info->setIsDiscovered(value.toBool()); break; + case TemperatureRole: + info->setTemperature(value.toDouble()); break; + case TopPRole: + info->setTopP(value.toDouble()); break; + case MinPRole: + info->setMinP(value.toDouble()); break; + case TopKRole: + info->setTopK(value.toInt()); break; + case MaxLengthRole: + info->setMaxLength(value.toInt()); break; + case PromptBatchSizeRole: + info->setPromptBatchSize(value.toInt()); break; + case ContextLengthRole: + info->setContextLength(value.toInt()); break; + case GpuLayersRole: + info->setGpuLayers(value.toInt()); break; + case RepeatPenaltyRole: + info->setRepeatPenalty(value.toDouble()); break; + case RepeatPenaltyTokensRole: + info->setRepeatPenaltyTokens(value.toInt()); break; + case PromptTemplateRole: + info->setPromptTemplate(value.toString()); break; + case SystemPromptRole: + info->setSystemPrompt(value.toString()); break; + case LikesRole: + info->setLikes(value.toInt()); break; + case DownloadsRole: + info->setDownloads(value.toInt()); break; + case RecencyRole: + info->setRecency(value.toDateTime()); break; + } } // Extra guarantee that these always remains in sync with filesystem - QFileInfo fileInfo(info->dirpath + info->filename()); - if (info->installed != fileInfo.exists()) { - info->installed = fileInfo.exists(); - updateInstalled = true; - } - QFileInfo incompleteInfo(incompleteDownloadPath(info->filename())); - if (info->isIncomplete != incompleteInfo.exists()) { - info->isIncomplete = incompleteInfo.exists(); - updateIncomplete = true; - } + const QFileInfo fileInfo(info->dirpath + info->filename()); + info->installed = fileInfo.exists(); + const QFileInfo incompleteInfo(incompleteDownloadPath(info->filename())); + info->isIncomplete = incompleteInfo.exists(); - std::stable_sort(m_models.begin(), m_models.end(), ModelList::lessThan); - modelSize = m_models.size(); + auto s = m_discoverSort; + auto d = m_discoverSortDirection; + std::stable_sort(m_models.begin(), m_models.end(), [s, d](const ModelInfo* lhs, const ModelInfo* rhs) { + return ModelList::lessThan(lhs, rhs, s, d); + }); } - emit dataChanged(createIndex(0, 0), createIndex(modelSize - 1, 0)); + emit dataChanged(createIndex(index, 0), createIndex(index, 0)); emit userDefaultModelListChanged(); } +void ModelList::resortModel() +{ + emit layoutAboutToBeChanged(); + { + QMutexLocker locker(&m_mutex); + auto s = m_discoverSort; + auto d = m_discoverSortDirection; + std::stable_sort(m_models.begin(), m_models.end(), [s, d](const ModelInfo* lhs, const ModelInfo* rhs) { + return ModelList::lessThan(lhs, rhs, s, d); + }); + } + emit layoutChanged(); +} + void ModelList::updateDataByFilename(const QString &filename, int role, const QVariant &value) { QVector modelsById; @@ -826,12 +1006,36 @@ QString ModelList::clone(const ModelInfo &model) return id; } -void ModelList::remove(const ModelInfo &model) +void ModelList::removeClone(const ModelInfo &model) { - Q_ASSERT(model.isClone); - if (!model.isClone) + Q_ASSERT(model.isClone()); + if (!model.isClone()) return; + removeInternal(model); + emit layoutChanged(); +} + +void ModelList::removeInstalled(const ModelInfo &model) +{ + // We only remove a model if it is installed and is discovered or sideloaded + if (!model.installed || !(model.isDiscovered() || model.description() == "" /*indicates sideloaded*/)) + return; + + // We shouldn't remove it if a discovered search is in progress or completed because the + // clearing of that search will erase it... + + // FIXME: This won't be true in a bit when save the installed portion to settings, then we'll need to + // use this... or at least unflag the installed bit... + if (m_discoverNumberOfResults) + return; + + removeInternal(model); + emit layoutChanged(); +} + +void ModelList::removeInternal(const ModelInfo &model) +{ const bool hasModel = contains(model.id()); Q_ASSERT(hasModel); if (!hasModel) { @@ -840,7 +1044,6 @@ void ModelList::remove(const ModelInfo &model) } int indexOfModel = 0; - int modelSizeAfter = 0; { QMutexLocker locker(&m_mutex); ModelInfo *info = m_modelMap.value(model.id()); @@ -852,10 +1055,8 @@ void ModelList::remove(const ModelInfo &model) ModelInfo *info = m_models.takeAt(indexOfModel); m_modelMap.remove(info->id()); delete info; - modelSizeAfter = m_models.size(); } endRemoveRows(); - emit dataChanged(index(0, 0), index(modelSizeAfter - 1, 0)); emit userDefaultModelListChanged(); MySettings::globalInstance()->eraseModel(model); } @@ -948,11 +1149,13 @@ void ModelList::updateModelsFromDirectory() } if (modelsById.isEmpty()) { - addModel(filename); + if (!contains(filename)) + addModel(filename); modelsById.append(filename); } for (const QString &id : modelsById) { + updateData(id, InstalledRole, true); updateData(id, FilenameRole, filename); // FIXME: WE should change this to use a consistent filename for online models updateData(id, OnlineRole, filename.startsWith("chatgpt-") || filename.startsWith("nomic-")); @@ -1127,7 +1330,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) QString requiresVersion = obj["requires"].toString(); QString deprecatedVersion = obj["deprecated"].toString(); QString url = obj["url"].toString(); - QByteArray modelMd5sum = obj["md5sum"].toString().toLatin1().constData(); + QByteArray modelHash = obj["md5sum"].toString().toLatin1().constData(); bool isDefault = obj.contains("isDefault") && obj["isDefault"] == QString("true"); bool disableGUI = obj.contains("disableGUI") && obj["disableGUI"] == QString("true"); QString description = obj["description"].toString(); @@ -1164,7 +1367,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) updateData(id, ModelList::NameRole, modelName); updateData(id, ModelList::FilenameRole, modelFilename); updateData(id, ModelList::FilesizeRole, modelFilesize); - updateData(id, ModelList::Md5sumRole, modelMd5sum); + updateData(id, ModelList::HashRole, modelHash); + updateData(id, ModelList::HashAlgorithmRole, ModelInfo::Md5); updateData(id, ModelList::DefaultRole, isDefault); updateData(id, ModelList::DescriptionRole, description); updateData(id, ModelList::RequiresVersionRole, requiresVersion); @@ -1282,6 +1486,21 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) } } +void ModelList::updateDiscoveredInstalled(const ModelInfo &info) +{ + updateData(info.id(), ModelList::InstalledRole, true); + updateData(info.id(), ModelList::IsDiscoveredRole, true); + updateData(info.id(), ModelList::NameRole, info.name()); + updateData(info.id(), ModelList::FilenameRole, info.filename()); + updateData(info.id(), ModelList::DescriptionRole, info.description()); + updateData(info.id(), ModelList::UrlRole, info.url()); + updateData(info.id(), ModelList::LikesRole, info.likes()); + updateData(info.id(), ModelList::DownloadsRole, info.downloads()); + updateData(info.id(), ModelList::RecencyRole, info.recency()); + updateData(info.id(), ModelList::QuantRole, info.quant()); + updateData(info.id(), ModelList::TypeRole, info.type()); +} + void ModelList::updateModelsFromSettings() { QSettings settings; @@ -1295,53 +1514,420 @@ void ModelList::updateModelsFromSettings() if (contains(id)) continue; - if (!settings.contains(g+ "/isClone")) - continue; - - Q_ASSERT(settings.contains(g + "/name")); - const QString name = settings.value(g + "/name").toString(); - Q_ASSERT(settings.contains(g + "/filename")); - const QString filename = settings.value(g + "/filename").toString(); - Q_ASSERT(settings.contains(g + "/temperature")); - const double temperature = settings.value(g + "/temperature").toDouble(); - Q_ASSERT(settings.contains(g + "/topP")); - const double topP = settings.value(g + "/topP").toDouble(); - Q_ASSERT(settings.contains(g + "/minP")); - const double minP = settings.value(g + "/minP").toDouble(); - Q_ASSERT(settings.contains(g + "/topK")); - const int topK = settings.value(g + "/topK").toInt(); - Q_ASSERT(settings.contains(g + "/maxLength")); - const int maxLength = settings.value(g + "/maxLength").toInt(); - Q_ASSERT(settings.contains(g + "/promptBatchSize")); - const int promptBatchSize = settings.value(g + "/promptBatchSize").toInt(); - Q_ASSERT(settings.contains(g + "/contextLength")); - const int contextLength = settings.value(g + "/contextLength").toInt(); - Q_ASSERT(settings.contains(g + "/gpuLayers")); - const int gpuLayers = settings.value(g + "/gpuLayers").toInt(); - Q_ASSERT(settings.contains(g + "/repeatPenalty")); - const double repeatPenalty = settings.value(g + "/repeatPenalty").toDouble(); - Q_ASSERT(settings.contains(g + "/repeatPenaltyTokens")); - const int repeatPenaltyTokens = settings.value(g + "/repeatPenaltyTokens").toInt(); - Q_ASSERT(settings.contains(g + "/promptTemplate")); - const QString promptTemplate = settings.value(g + "/promptTemplate").toString(); - Q_ASSERT(settings.contains(g + "/systemPrompt")); - const QString systemPrompt = settings.value(g + "/systemPrompt").toString(); - addModel(id); - updateData(id, ModelList::IsCloneRole, true); - updateData(id, ModelList::NameRole, name); - updateData(id, ModelList::FilenameRole, filename); - updateData(id, ModelList::TemperatureRole, temperature); - updateData(id, ModelList::TopPRole, topP); - updateData(id, ModelList::MinPRole, minP); - updateData(id, ModelList::TopKRole, topK); - updateData(id, ModelList::MaxLengthRole, maxLength); - updateData(id, ModelList::PromptBatchSizeRole, promptBatchSize); - updateData(id, ModelList::ContextLengthRole, contextLength); - updateData(id, ModelList::GpuLayersRole, gpuLayers); - updateData(id, ModelList::RepeatPenaltyRole, repeatPenalty); - updateData(id, ModelList::RepeatPenaltyTokensRole, repeatPenaltyTokens); - updateData(id, ModelList::PromptTemplateRole, promptTemplate); - updateData(id, ModelList::SystemPromptRole, systemPrompt); + + if (settings.contains(g + "/name")) { + const QString name = settings.value(g + "/name").toString(); + updateData(id, ModelList::NameRole, name); + } + if (settings.contains(g + "/filename")) { + const QString filename = settings.value(g + "/filename").toString(); + updateData(id, ModelList::FilenameRole, filename); + } + if (settings.contains(g + "/description")) { + const QString d = settings.value(g + "/description").toString(); + updateData(id, ModelList::DescriptionRole, d); + } + if (settings.contains(g + "/url")) { + const QString u = settings.value(g + "/url").toString(); + updateData(id, ModelList::UrlRole, u); + } + if (settings.contains(g + "/quant")) { + const QString q = settings.value(g + "/quant").toString(); + updateData(id, ModelList::QuantRole, q); + } + if (settings.contains(g + "/type")) { + const QString t = settings.value(g + "/type").toString(); + updateData(id, ModelList::TypeRole, t); + } + if (settings.contains(g + "/isClone")) { + const bool b = settings.value(g + "/isClone").toBool(); + updateData(id, ModelList::IsCloneRole, b); + } + if (settings.contains(g + "/isDiscovered")) { + const bool b = settings.value(g + "/isDiscovered").toBool(); + updateData(id, ModelList::IsDiscoveredRole, b); + } + if (settings.contains(g + "/likes")) { + const int l = settings.value(g + "/likes").toInt(); + updateData(id, ModelList::LikesRole, l); + } + if (settings.contains(g + "/downloads")) { + const int d = settings.value(g + "/downloads").toInt(); + updateData(id, ModelList::DownloadsRole, d); + } + if (settings.contains(g + "/recency")) { + const QDateTime r = settings.value(g + "/recency").toDateTime(); + updateData(id, ModelList::RecencyRole, r); + } + if (settings.contains(g + "/temperature")) { + const double temperature = settings.value(g + "/temperature").toDouble(); + updateData(id, ModelList::TemperatureRole, temperature); + } + if (settings.contains(g + "/topP")) { + const double topP = settings.value(g + "/topP").toDouble(); + updateData(id, ModelList::TopPRole, topP); + } + if (settings.contains(g + "/minP")) { + const double minP = settings.value(g + "/minP").toDouble(); + updateData(id, ModelList::MinPRole, minP); + } + if (settings.contains(g + "/topK")) { + const int topK = settings.value(g + "/topK").toInt(); + updateData(id, ModelList::TopKRole, topK); + } + if (settings.contains(g + "/maxLength")) { + const int maxLength = settings.value(g + "/maxLength").toInt(); + updateData(id, ModelList::MaxLengthRole, maxLength); + } + if (settings.contains(g + "/promptBatchSize")) { + const int promptBatchSize = settings.value(g + "/promptBatchSize").toInt(); + updateData(id, ModelList::PromptBatchSizeRole, promptBatchSize); + } + if (settings.contains(g + "/contextLength")) { + const int contextLength = settings.value(g + "/contextLength").toInt(); + updateData(id, ModelList::ContextLengthRole, contextLength); + } + if (settings.contains(g + "/gpuLayers")) { + const int gpuLayers = settings.value(g + "/gpuLayers").toInt(); + updateData(id, ModelList::GpuLayersRole, gpuLayers); + } + if (settings.contains(g + "/repeatPenalty")) { + const double repeatPenalty = settings.value(g + "/repeatPenalty").toDouble(); + updateData(id, ModelList::RepeatPenaltyRole, repeatPenalty); + } + if (settings.contains(g + "/repeatPenaltyTokens")) { + const int repeatPenaltyTokens = settings.value(g + "/repeatPenaltyTokens").toInt(); + updateData(id, ModelList::RepeatPenaltyTokensRole, repeatPenaltyTokens); + } + if (settings.contains(g + "/promptTemplate")) { + const QString promptTemplate = settings.value(g + "/promptTemplate").toString(); + updateData(id, ModelList::PromptTemplateRole, promptTemplate); + } + if (settings.contains(g + "/systemPrompt")) { + const QString systemPrompt = settings.value(g + "/systemPrompt").toString(); + updateData(id, ModelList::SystemPromptRole, systemPrompt); + } } } + +int ModelList::discoverLimit() const +{ + return m_discoverLimit; +} + +void ModelList::setDiscoverLimit(int limit) +{ + if (m_discoverLimit == limit) + return; + m_discoverLimit = limit; + emit discoverLimitChanged(); +} + +int ModelList::discoverSortDirection() const +{ + return m_discoverSortDirection; +} + +void ModelList::setDiscoverSortDirection(int direction) +{ + if (m_discoverSortDirection == direction || (direction != 1 && direction != -1)) + return; + m_discoverSortDirection = direction; + emit discoverSortDirectionChanged(); + resortModel(); +} + +ModelList::DiscoverSort ModelList::discoverSort() const +{ + return m_discoverSort; +} + +void ModelList::setDiscoverSort(DiscoverSort sort) +{ + if (m_discoverSort == sort) + return; + m_discoverSort = sort; + emit discoverSortChanged(); + resortModel(); +} + +void ModelList::clearDiscoveredModels() +{ + // NOTE: This could be made much more efficient + QList infos; + { + QMutexLocker locker(&m_mutex); + for (ModelInfo *info : m_models) + if (info->isDiscovered()) + infos.append(*info); + } + for (ModelInfo &info : infos) + removeInternal(info); + emit layoutChanged(); +} + +float ModelList::discoverProgress() const +{ + if (!m_discoverNumberOfResults) + return 0.0f; + return m_discoverResultsCompleted / float(m_discoverNumberOfResults); +} + +bool ModelList::discoverInProgress() const +{ + return m_discoverInProgress; +} + +void ModelList::discoverSearch(const QString &search) +{ + Q_ASSERT(!m_discoverInProgress); + + clearDiscoveredModels(); + + m_discoverNumberOfResults = 0; + m_discoverResultsCompleted = 0; + discoverProgressChanged(); + + if (search.isEmpty()) { + return; + } + + m_discoverInProgress = true; + emit discoverInProgressChanged(); + + QStringList searchParams = search.split(QRegularExpression("\\s+")); // split by whitespace + QString searchString = QString("search=%1&").arg(searchParams.join('+')); + QString limitString = m_discoverLimit > 0 ? QString("limit=%1&").arg(m_discoverLimit) : QString(); + + QString sortString; + switch (m_discoverSort) { + case Default: break; + case Likes: + sortString = "sort=likes&"; break; + case Downloads: + sortString = "sort=downloads&"; break; + case Recent: + sortString = "sort=lastModified&"; break; + } + + QString directionString = !sortString.isEmpty() ? QString("direction=%1&").arg(m_discoverSortDirection) : QString(); + + QUrl hfUrl(QString("https://huggingface.co/api/models?filter=gguf&%1%2%3%4full=true&config=true").arg(searchString).arg(limitString).arg(sortString).arg(directionString)); + + QNetworkRequest request(hfUrl); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + QNetworkReply *reply = m_networkManager.get(request); + connect(qApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort); + connect(reply, &QNetworkReply::finished, this, &ModelList::handleDiscoveryFinished); + connect(reply, &QNetworkReply::errorOccurred, this, &ModelList::handleDiscoveryErrorOccurred); +} + +void ModelList::handleDiscoveryFinished() +{ + QNetworkReply *jsonReply = qobject_cast(sender()); + if (!jsonReply) + return; + + QByteArray jsonData = jsonReply->readAll(); + parseDiscoveryJsonFile(jsonData); + jsonReply->deleteLater(); +} + +void ModelList::handleDiscoveryErrorOccurred(QNetworkReply::NetworkError code) +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) + return; + qWarning() << QString("ERROR: Discovery failed with error code \"%1-%2\"") + .arg(code).arg(reply->errorString()).toStdString(); +} + +enum QuantType { + Q4_0 = 0, + Q4_1, + F16, + F32, + Unknown +}; + +QuantType toQuantType(const QString& filename) +{ + QString lowerCaseFilename = filename.toLower(); + if (lowerCaseFilename.contains("q4_0")) return Q4_0; + if (lowerCaseFilename.contains("q4_1")) return Q4_1; + if (lowerCaseFilename.contains("f16")) return F16; + if (lowerCaseFilename.contains("f32")) return F32; + return Unknown; +} + +QString toQuantString(const QString& filename) +{ + QString lowerCaseFilename = filename.toLower(); + if (lowerCaseFilename.contains("q4_0")) return "q4_0"; + if (lowerCaseFilename.contains("q4_1")) return "q4_1"; + if (lowerCaseFilename.contains("f16")) return "f16"; + if (lowerCaseFilename.contains("f32")) return "f32"; + return QString(); +} + +void ModelList::parseDiscoveryJsonFile(const QByteArray &jsonData) +{ + QJsonParseError err; + QJsonDocument document = QJsonDocument::fromJson(jsonData, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "ERROR: Couldn't parse: " << jsonData << err.errorString(); + m_discoverNumberOfResults = 0; + m_discoverResultsCompleted = 0; + discoverProgressChanged(); + m_discoverInProgress = false; + emit discoverInProgressChanged(); + return; + } + + QJsonArray jsonArray = document.array(); + + for (const QJsonValue &value : jsonArray) { + QJsonObject obj = value.toObject(); + QJsonDocument jsonDocument(obj); + QByteArray jsonData = jsonDocument.toJson(); + + QString repo_id = obj["id"].toString(); + QJsonArray siblingsArray = obj["siblings"].toArray(); + QList> filteredAndSortedFilenames; + for (const QJsonValue &sibling : siblingsArray) { + + QJsonObject s = sibling.toObject(); + QString filename = s["rfilename"].toString(); + if (!filename.endsWith("gguf")) + continue; + + QuantType quant = toQuantType(filename); + if (quant != Unknown) + filteredAndSortedFilenames.append(qMakePair(quant, filename)); + } + + if (filteredAndSortedFilenames.isEmpty()) + continue; + + std::sort(filteredAndSortedFilenames.begin(), filteredAndSortedFilenames.end(), + [](const QPair& a, const QPair& b) { + return a.first < b.first; + }); + + QPair file = filteredAndSortedFilenames.first(); + QString filename = file.second; + ++m_discoverNumberOfResults; + + QUrl url(QString("https://huggingface.co/%1/resolve/main/%2").arg(repo_id).arg(filename)); + QNetworkRequest request(url); + request.setRawHeader("Accept-Encoding", "identity"); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); + request.setAttribute(QNetworkRequest::User, jsonData); + request.setAttribute(QNetworkRequest::UserMax, filename); + QNetworkReply *reply = m_networkManager.head(request); + connect(qApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort); + connect(reply, &QNetworkReply::finished, this, &ModelList::handleDiscoveryItemFinished); + connect(reply, &QNetworkReply::errorOccurred, this, &ModelList::handleDiscoveryItemErrorOccurred); + } + + emit discoverProgressChanged(); + if (!m_discoverNumberOfResults) { + m_discoverInProgress = false; + emit discoverInProgressChanged();; + } +} + +void ModelList::handleDiscoveryItemFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) + return; + + QVariant replyCustomData = reply->request().attribute(QNetworkRequest::User); + QByteArray customDataByteArray = replyCustomData.toByteArray(); + QJsonDocument customJsonDocument = QJsonDocument::fromJson(customDataByteArray); + QJsonObject obj = customJsonDocument.object(); + + QString repo_id = obj["id"].toString(); + QString modelName = obj["modelId"].toString(); + QString author = obj["author"].toString(); + QDateTime lastModified = QDateTime::fromString(obj["lastModified"].toString(), Qt::ISODateWithMs); + int likes = obj["likes"].toInt(); + int downloads = obj["downloads"].toInt(); + QJsonObject config = obj["config"].toObject(); + QString type = config["model_type"].toString(); + + QByteArray repoCommitHeader = reply->rawHeader("X-Repo-Commit"); + QByteArray linkedSizeHeader = reply->rawHeader("X-Linked-Size"); + QByteArray linkedEtagHeader = reply->rawHeader("X-Linked-Etag"); + // For some reason these seem to contain quotation marks ewww + linkedEtagHeader.replace("\"", ""); + linkedEtagHeader.replace("\'", ""); + QString locationHeader = reply->header(QNetworkRequest::LocationHeader).toString(); + + QString repoCommit = QString::fromUtf8(repoCommitHeader); + QString linkedSize = QString::fromUtf8(linkedSizeHeader); + QString linkedEtag = QString::fromUtf8(linkedEtagHeader); + QString url = locationHeader; + QString modelFilename = reply->request().attribute(QNetworkRequest::UserMax).toString(); + QString modelFilesize = linkedSize; + modelFilesize = ModelList::toFileSize(modelFilesize.toULongLong()); + + QString description = tr("Created by %1.
    " + "
  • Published on %2." + "
  • This model has %3 likes." + "
  • This model has %4 downloads." + "
  • More info can be found here.
") + .arg(author) + .arg(lastModified.toString("ddd MMMM d, yyyy")) + .arg(likes) + .arg(downloads) + .arg(repo_id); + + const QString id = modelFilename; + Q_ASSERT(!id.isEmpty()); + + if (contains(modelFilename)) + changeId(modelFilename, id); + + if (!contains(id)) + addModel(id); + + QVector> data; + data.append(qMakePair(ModelList::NameRole, modelName)); + data.append(qMakePair(ModelList::FilenameRole, modelFilename)); + data.append(qMakePair(ModelList::FilesizeRole, modelFilesize)); + data.append(qMakePair(ModelList::DescriptionRole, description)); + data.append(qMakePair(ModelList::IsDiscoveredRole, true)); + data.append(qMakePair(ModelList::UrlRole, url)); + data.append(qMakePair(ModelList::LikesRole, likes)); + data.append(qMakePair(ModelList::DownloadsRole, downloads)); + data.append(qMakePair(ModelList::RecencyRole, lastModified)); + data.append(qMakePair(ModelList::QuantRole, toQuantString(modelFilename))); + data.append(qMakePair(ModelList::TypeRole, type)); + data.append(qMakePair(ModelList::HashRole, linkedEtagHeader)); + data.append(qMakePair(ModelList::HashAlgorithmRole, ModelInfo::Sha256)); + updateData(id, data); + + ++m_discoverResultsCompleted; + emit discoverProgressChanged(); + + if (discoverProgress() >= 1.0) { + emit layoutChanged(); + m_discoverInProgress = false; + emit discoverInProgressChanged();; + } + + reply->deleteLater(); +} + +void ModelList::handleDiscoveryItemErrorOccurred(QNetworkReply::NetworkError code) +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) + return; + + qWarning() << QString("ERROR: Discovery item failed with error code \"%1-%2\"") + .arg(code).arg(reply->errorString()).toStdString(); +} diff --git a/gpt4all-chat/modellist.h b/gpt4all-chat/modellist.h index 4bfc6beb..c712f862 100644 --- a/gpt4all-chat/modellist.h +++ b/gpt4all-chat/modellist.h @@ -11,16 +11,17 @@ struct ModelInfo { Q_PROPERTY(QString filename READ filename WRITE setFilename) Q_PROPERTY(QString dirpath MEMBER dirpath) Q_PROPERTY(QString filesize MEMBER filesize) - Q_PROPERTY(QByteArray md5sum MEMBER md5sum) + Q_PROPERTY(QByteArray hash MEMBER hash) + Q_PROPERTY(HashAlgorithm hashAlgorithm MEMBER hashAlgorithm) Q_PROPERTY(bool calcHash MEMBER calcHash) Q_PROPERTY(bool installed MEMBER installed) Q_PROPERTY(bool isDefault MEMBER isDefault) Q_PROPERTY(bool disableGUI MEMBER disableGUI) Q_PROPERTY(bool isOnline MEMBER isOnline) - Q_PROPERTY(QString description MEMBER description) + Q_PROPERTY(QString description READ description WRITE setDescription) Q_PROPERTY(QString requiresVersion MEMBER requiresVersion) Q_PROPERTY(QString deprecatedVersion MEMBER deprecatedVersion) - Q_PROPERTY(QString url MEMBER url) + Q_PROPERTY(QString url READ url WRITE setUrl) Q_PROPERTY(qint64 bytesReceived MEMBER bytesReceived) Q_PROPERTY(qint64 bytesTotal MEMBER bytesTotal) Q_PROPERTY(qint64 timestamp MEMBER timestamp) @@ -31,9 +32,10 @@ struct ModelInfo { Q_PROPERTY(QString order MEMBER order) Q_PROPERTY(int ramrequired MEMBER ramrequired) Q_PROPERTY(QString parameters MEMBER parameters) - Q_PROPERTY(QString quant MEMBER quant) - Q_PROPERTY(QString type MEMBER type) - Q_PROPERTY(bool isClone MEMBER isClone) + Q_PROPERTY(QString quant READ quant WRITE setQuant) + Q_PROPERTY(QString type READ type WRITE setType) + Q_PROPERTY(bool isClone READ isClone WRITE setIsClone) + Q_PROPERTY(bool isDiscovered READ isDiscovered WRITE setIsDiscovered) Q_PROPERTY(double temperature READ temperature WRITE setTemperature) Q_PROPERTY(double topP READ topP WRITE setTopP) Q_PROPERTY(double minP READ minP WRITE setMinP) @@ -48,8 +50,16 @@ struct ModelInfo { Q_PROPERTY(int repeatPenaltyTokens READ repeatPenaltyTokens WRITE setRepeatPenaltyTokens) Q_PROPERTY(QString promptTemplate READ promptTemplate WRITE setPromptTemplate) Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt) + Q_PROPERTY(int likes READ likes WRITE setLikes) + Q_PROPERTY(int downloads READ downloads WRITE setDownloads) + Q_PROPERTY(QDateTime recency READ recency WRITE setRecency) public: + enum HashAlgorithm { + Md5, + Sha256 + }; + QString id() const; void setId(const QString &id); @@ -59,18 +69,44 @@ public: QString filename() const; void setFilename(const QString &name); + QString description() const; + void setDescription(const QString &d); + + QString url() const; + void setUrl(const QString &u); + + QString quant() const; + void setQuant(const QString &q); + + QString type() const; + void setType(const QString &t); + + bool isClone() const; + void setIsClone(bool b); + + bool isDiscovered() const; + void setIsDiscovered(bool b); + + int likes() const; + void setLikes(int l); + + int downloads() const; + void setDownloads(int d); + + QDateTime recency() const; + void setRecency(const QDateTime &r); + QString dirpath; QString filesize; - QByteArray md5sum; + QByteArray hash; + HashAlgorithm hashAlgorithm; bool calcHash = false; bool installed = false; bool isDefault = false; bool isOnline = false; bool disableGUI = false; - QString description; QString requiresVersion; QString deprecatedVersion; - QString url; qint64 bytesReceived = 0; qint64 bytesTotal = 0; qint64 timestamp = 0; @@ -79,11 +115,8 @@ public: bool isIncomplete = false; QString downloadError; QString order; - int ramrequired = 0; + int ramrequired = -1; QString parameters; - QString quant; - QString type; - bool isClone = false; bool operator==(const ModelInfo &other) const { return m_id == other.m_id; @@ -116,10 +149,21 @@ public: QString systemPrompt() const; void setSystemPrompt(const QString &p); + bool shouldSaveMetadata() const; + private: QString m_id; QString m_name; QString m_filename; + QString m_description; + QString m_url; + QString m_quant; + QString m_type; + bool m_isClone = false; + bool m_isDiscovered = false; + int m_likes = -1; + int m_downloads = -1; + QDateTime m_recency; double m_temperature = 0.7; double m_topP = 0.4; double m_minP = 0.0; @@ -183,6 +227,8 @@ public: bool isExpanded() const; void setExpanded(bool expanded); + Q_INVOKABLE void discoverAndFilter(const QString &discover); + Q_SIGNALS: void countChanged(); @@ -195,6 +241,7 @@ Q_SIGNALS: private: bool m_expanded; int m_limit; + QString m_discoverFilter; }; class ModelList : public QAbstractListModel @@ -207,17 +254,30 @@ class ModelList : public QAbstractListModel Q_PROPERTY(DownloadableModels* downloadableModels READ downloadableModels NOTIFY downloadableModelsChanged) Q_PROPERTY(QList userDefaultModelList READ userDefaultModelList NOTIFY userDefaultModelListChanged) Q_PROPERTY(bool asyncModelRequestOngoing READ asyncModelRequestOngoing NOTIFY asyncModelRequestOngoingChanged) + Q_PROPERTY(int discoverLimit READ discoverLimit WRITE setDiscoverLimit NOTIFY discoverLimitChanged) + Q_PROPERTY(int discoverSortDirection READ discoverSortDirection WRITE setDiscoverSortDirection NOTIFY discoverSortDirectionChanged) + Q_PROPERTY(DiscoverSort discoverSort READ discoverSort WRITE setDiscoverSort NOTIFY discoverSortChanged) + Q_PROPERTY(float discoverProgress READ discoverProgress NOTIFY discoverProgressChanged) + Q_PROPERTY(bool discoverInProgress READ discoverInProgress NOTIFY discoverInProgressChanged) public: static ModelList *globalInstance(); + enum DiscoverSort { + Default, + Likes, + Downloads, + Recent + }; + enum Roles { IdRole = Qt::UserRole + 1, NameRole, FilenameRole, DirpathRole, FilesizeRole, - Md5sumRole, + HashRole, + HashAlgorithmRole, CalcHashRole, InstalledRole, DefaultRole, @@ -240,6 +300,7 @@ public: QuantRole, TypeRole, IsCloneRole, + IsDiscoveredRole, TemperatureRole, TopPRole, TopKRole, @@ -252,6 +313,9 @@ public: PromptTemplateRole, SystemPromptRole, MinPRole, + LikesRole, + DownloadsRole, + RecencyRole }; QHash roleNames() const override @@ -262,7 +326,8 @@ public: roles[FilenameRole] = "filename"; roles[DirpathRole] = "dirpath"; roles[FilesizeRole] = "filesize"; - roles[Md5sumRole] = "md5sum"; + roles[HashRole] = "hash"; + roles[HashAlgorithmRole] = "hashAlgorithm"; roles[CalcHashRole] = "calcHash"; roles[InstalledRole] = "installed"; roles[DefaultRole] = "isDefault"; @@ -285,6 +350,7 @@ public: roles[QuantRole] = "quant"; roles[TypeRole] = "type"; roles[IsCloneRole] = "isClone"; + roles[IsDiscoveredRole] = "isDiscovered"; roles[TemperatureRole] = "temperature"; roles[TopPRole] = "topP"; roles[MinPRole] = "minP"; @@ -297,6 +363,9 @@ public: roles[RepeatPenaltyTokensRole] = "repeatPenaltyTokens"; roles[PromptTemplateRole] = "promptTemplate"; roles[SystemPromptRole] = "systemPrompt"; + roles[LikesRole] = "likes"; + roles[DownloadsRole] = "downloads"; + roles[RecencyRole] = "recency"; return roles; } @@ -306,6 +375,7 @@ public: QVariant dataByFilename(const QString &filename, int role) const; void updateData(const QString &id, int role, const QVariant &value); void updateDataByFilename(const QString &filename, int role, const QVariant &value); + void updateData(const QString &id, const QVector> &data); int count() const { return m_models.size(); } @@ -315,7 +385,8 @@ public: Q_INVOKABLE ModelInfo modelInfoByFilename(const QString &filename) const; Q_INVOKABLE bool isUniqueName(const QString &name) const; Q_INVOKABLE QString clone(const ModelInfo &model); - Q_INVOKABLE void remove(const ModelInfo &model); + Q_INVOKABLE void removeClone(const ModelInfo &model); + Q_INVOKABLE void removeInstalled(const ModelInfo &model); ModelInfo defaultModelInfo() const; int defaultEmbeddingModelIndex() const; @@ -345,6 +416,21 @@ public: bool asyncModelRequestOngoing() const { return m_asyncModelRequestOngoing; } void updateModelsFromDirectory(); + void updateDiscoveredInstalled(const ModelInfo &info); + + int discoverLimit() const; + void setDiscoverLimit(int limit); + + int discoverSortDirection() const; + void setDiscoverSortDirection(int direction); // -1 or 1 + + DiscoverSort discoverSort() const; + void setDiscoverSort(DiscoverSort sort); + + float discoverProgress() const; + bool discoverInProgress() const; + + Q_INVOKABLE void discoverSearch(const QString &discover); Q_SIGNALS: void countChanged(); @@ -354,22 +440,35 @@ Q_SIGNALS: void userDefaultModelListChanged(); void asyncModelRequestOngoingChanged(); void defaultEmbeddingModelIndexChanged(); + void discoverLimitChanged(); + void discoverSortDirectionChanged(); + void discoverSortChanged(); + void discoverProgressChanged(); + void discoverInProgressChanged(); private Q_SLOTS: + void resortModel(); void updateModelsFromJson(); void updateModelsFromJsonAsync(); void updateModelsFromSettings(); void updateDataForSettings(); void handleModelsJsonDownloadFinished(); void handleModelsJsonDownloadErrorOccurred(QNetworkReply::NetworkError code); + void handleDiscoveryFinished(); + void handleDiscoveryErrorOccurred(QNetworkReply::NetworkError code); + void handleDiscoveryItemFinished(); + void handleDiscoveryItemErrorOccurred(QNetworkReply::NetworkError code); void handleSslErrors(QNetworkReply *reply, const QList &errors); private: + void removeInternal(const ModelInfo &model); + void clearDiscoveredModels(); QString modelDirPath(const QString &modelName, bool isOnline); int indexForModel(ModelInfo *model); QVariant dataInternal(const ModelInfo *info, int role) const; - static bool lessThan(const ModelInfo* a, const ModelInfo* b); + static bool lessThan(const ModelInfo* a, const ModelInfo* b, DiscoverSort s, int d); void parseModelsJsonFile(const QByteArray &jsonData, bool save); + void parseDiscoveryJsonFile(const QByteArray &jsonData); QString uniqueModelName(const ModelInfo &model) const; private: @@ -381,8 +480,14 @@ private: QList m_models; QHash m_modelMap; bool m_asyncModelRequestOngoing; + int m_discoverLimit; + int m_discoverSortDirection; + DiscoverSort m_discoverSort; + int m_discoverNumberOfResults; + int m_discoverResultsCompleted; + bool m_discoverInProgress; -private: +protected: explicit ModelList(); ~ModelList() {} friend class MyModelList; diff --git a/gpt4all-chat/mysettings.cpp b/gpt4all-chat/mysettings.cpp index 23beda2e..9d6dc5cd 100644 --- a/gpt4all-chat/mysettings.cpp +++ b/gpt4all-chat/mysettings.cpp @@ -140,12 +140,10 @@ void MySettings::setModelName(const ModelInfo &m, const QString &name, bool forc return; QSettings setting; - if ((m.m_name == name || m.m_filename == name) && !m.isClone) + if ((m.m_name == name || m.m_filename == name) && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/name"); else setting.setValue(QString("model-%1").arg(m.id()) + "/name", name); - if (m.isClone) - setting.setValue(QString("model-%1").arg(m.id()) + "/isClone", "true"); setting.sync(); if (!force) emit nameChanged(m); @@ -164,7 +162,7 @@ void MySettings::setModelFilename(const ModelInfo &m, const QString &filename, b return; QSettings setting; - if (m.m_filename == filename && !m.isClone) + if (m.m_filename == filename && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/filename"); else setting.setValue(QString("model-%1").arg(m.id()) + "/filename", filename); @@ -173,6 +171,186 @@ void MySettings::setModelFilename(const ModelInfo &m, const QString &filename, b emit filenameChanged(m); } +QString MySettings::modelDescription(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/description", m.m_description).toString(); +} + +void MySettings::setModelDescription(const ModelInfo &m, const QString &d, bool force) +{ + if ((modelDescription(m) == d || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_description == d && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/description"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/description", d); + setting.sync(); +} + +QString MySettings::modelUrl(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/url", m.m_url).toString(); +} + +void MySettings::setModelUrl(const ModelInfo &m, const QString &u, bool force) +{ + if ((modelUrl(m) == u || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_url == u && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/url"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/url", u); + setting.sync(); +} + +QString MySettings::modelQuant(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/quant", m.m_quant).toString(); +} + +void MySettings::setModelQuant(const ModelInfo &m, const QString &q, bool force) +{ + if ((modelUrl(m) == q || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_quant == q && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/quant"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/quant", q); + setting.sync(); +} + +QString MySettings::modelType(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/type", m.m_type).toString(); +} + +void MySettings::setModelType(const ModelInfo &m, const QString &t, bool force) +{ + if ((modelType(m) == t || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_type == t && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/type"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/type", t); + setting.sync(); +} + +bool MySettings::modelIsClone(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/isClone", m.m_isClone).toBool(); +} + +void MySettings::setModelIsClone(const ModelInfo &m, bool b, bool force) +{ + if ((modelIsClone(m) == b || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_isClone == b && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/isClone"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/isClone", b); + setting.sync(); +} + +bool MySettings::modelIsDiscovered(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/isDiscovered", m.m_isDiscovered).toBool(); +} + +void MySettings::setModelIsDiscovered(const ModelInfo &m, bool b, bool force) +{ + if ((modelIsDiscovered(m) == b || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_isDiscovered == b && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/isDiscovered"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/isDiscovered", b); + setting.sync(); +} + +int MySettings::modelLikes(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/likes", m.m_likes).toInt(); +} + +void MySettings::setModelLikes(const ModelInfo &m, int l, bool force) +{ + if ((modelLikes(m) == l || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_likes == l && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/likes"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/likes", l); + setting.sync(); +} + +int MySettings::modelDownloads(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/downloads", m.m_downloads).toInt(); +} + +void MySettings::setModelDownloads(const ModelInfo &m, int d, bool force) +{ + if ((modelDownloads(m) == d || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_downloads == d && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/downloads"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/downloads", d); + setting.sync(); +} + +QDateTime MySettings::modelRecency(const ModelInfo &m) const +{ + QSettings setting; + setting.sync(); + return setting.value(QString("model-%1").arg(m.id()) + "/recency", m.m_recency).toDateTime(); +} + +void MySettings::setModelRecency(const ModelInfo &m, const QDateTime &r, bool force) +{ + if ((modelRecency(m) == r || m.id().isEmpty()) && !force) + return; + + QSettings setting; + if (m.m_recency == r && !m.shouldSaveMetadata()) + setting.remove(QString("model-%1").arg(m.id()) + "/recency"); + else + setting.setValue(QString("model-%1").arg(m.id()) + "/recency", r); + setting.sync(); +} + double MySettings::modelTemperature(const ModelInfo &m) const { QSettings setting; @@ -186,7 +364,7 @@ void MySettings::setModelTemperature(const ModelInfo &m, double t, bool force) return; QSettings setting; - if (m.m_temperature == t && !m.isClone) + if (m.m_temperature == t && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/temperature"); else setting.setValue(QString("model-%1").arg(m.id()) + "/temperature", t); @@ -215,7 +393,7 @@ void MySettings::setModelTopP(const ModelInfo &m, double p, bool force) return; QSettings setting; - if (m.m_topP == p && !m.isClone) + if (m.m_topP == p && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/topP"); else setting.setValue(QString("model-%1").arg(m.id()) + "/topP", p); @@ -230,7 +408,7 @@ void MySettings::setModelMinP(const ModelInfo &m, double p, bool force) return; QSettings setting; - if (m.m_minP == p && !m.isClone) + if (m.m_minP == p && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/minP"); else setting.setValue(QString("model-%1").arg(m.id()) + "/minP", p); @@ -252,7 +430,7 @@ void MySettings::setModelTopK(const ModelInfo &m, int k, bool force) return; QSettings setting; - if (m.m_topK == k && !m.isClone) + if (m.m_topK == k && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/topK"); else setting.setValue(QString("model-%1").arg(m.id()) + "/topK", k); @@ -274,7 +452,7 @@ void MySettings::setModelMaxLength(const ModelInfo &m, int l, bool force) return; QSettings setting; - if (m.m_maxLength == l && !m.isClone) + if (m.m_maxLength == l && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/maxLength"); else setting.setValue(QString("model-%1").arg(m.id()) + "/maxLength", l); @@ -296,7 +474,7 @@ void MySettings::setModelPromptBatchSize(const ModelInfo &m, int s, bool force) return; QSettings setting; - if (m.m_promptBatchSize == s && !m.isClone) + if (m.m_promptBatchSize == s && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/promptBatchSize"); else setting.setValue(QString("model-%1").arg(m.id()) + "/promptBatchSize", s); @@ -318,7 +496,7 @@ void MySettings::setModelContextLength(const ModelInfo &m, int l, bool force) return; QSettings setting; - if (m.m_contextLength == l && !m.isClone) + if (m.m_contextLength == l && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/contextLength"); else setting.setValue(QString("model-%1").arg(m.id()) + "/contextLength", l); @@ -340,7 +518,7 @@ void MySettings::setModelGpuLayers(const ModelInfo &m, int l, bool force) return; QSettings setting; - if (m.m_gpuLayers == l && !m.isClone) + if (m.m_gpuLayers == l && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/gpuLayers"); else setting.setValue(QString("model-%1").arg(m.id()) + "/gpuLayers", l); @@ -362,7 +540,7 @@ void MySettings::setModelRepeatPenalty(const ModelInfo &m, double p, bool force) return; QSettings setting; - if (m.m_repeatPenalty == p && !m.isClone) + if (m.m_repeatPenalty == p && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/repeatPenalty"); else setting.setValue(QString("model-%1").arg(m.id()) + "/repeatPenalty", p); @@ -384,7 +562,7 @@ void MySettings::setModelRepeatPenaltyTokens(const ModelInfo &m, int t, bool for return; QSettings setting; - if (m.m_repeatPenaltyTokens == t && !m.isClone) + if (m.m_repeatPenaltyTokens == t && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/repeatPenaltyTokens"); else setting.setValue(QString("model-%1").arg(m.id()) + "/repeatPenaltyTokens", t); @@ -406,7 +584,7 @@ void MySettings::setModelPromptTemplate(const ModelInfo &m, const QString &t, bo return; QSettings setting; - if (m.m_promptTemplate == t && !m.isClone) + if (m.m_promptTemplate == t && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/promptTemplate"); else setting.setValue(QString("model-%1").arg(m.id()) + "/promptTemplate", t); @@ -428,7 +606,7 @@ void MySettings::setModelSystemPrompt(const ModelInfo &m, const QString &p, bool return; QSettings setting; - if (m.m_systemPrompt == p && !m.isClone) + if (m.m_systemPrompt == p && !m.shouldSaveMetadata()) setting.remove(QString("model-%1").arg(m.id()) + "/systemPrompt"); else setting.setValue(QString("model-%1").arg(m.id()) + "/systemPrompt", p); diff --git a/gpt4all-chat/mysettings.h b/gpt4all-chat/mysettings.h index 94f86c32..d137c583 100644 --- a/gpt4all-chat/mysettings.h +++ b/gpt4all-chat/mysettings.h @@ -43,6 +43,26 @@ public: Q_INVOKABLE void setModelName(const ModelInfo &m, const QString &name, bool force = false); QString modelFilename(const ModelInfo &m) const; Q_INVOKABLE void setModelFilename(const ModelInfo &m, const QString &filename, bool force = false); + + QString modelDescription(const ModelInfo &m) const; + void setModelDescription(const ModelInfo &m, const QString &d, bool force = false); + QString modelUrl(const ModelInfo &m) const; + void setModelUrl(const ModelInfo &m, const QString &u, bool force = false); + QString modelQuant(const ModelInfo &m) const; + void setModelQuant(const ModelInfo &m, const QString &q, bool force = false); + QString modelType(const ModelInfo &m) const; + void setModelType(const ModelInfo &m, const QString &t, bool force = false); + bool modelIsClone(const ModelInfo &m) const; + void setModelIsClone(const ModelInfo &m, bool b, bool force = false); + bool modelIsDiscovered(const ModelInfo &m) const; + void setModelIsDiscovered(const ModelInfo &m, bool b, bool force = false); + int modelLikes(const ModelInfo &m) const; + void setModelLikes(const ModelInfo &m, int l, bool force = false); + int modelDownloads(const ModelInfo &m) const; + void setModelDownloads(const ModelInfo &m, int d, bool force = false); + QDateTime modelRecency(const ModelInfo &m) const; + void setModelRecency(const ModelInfo &m, const QDateTime &r, bool force = false); + double modelTemperature(const ModelInfo &m) const; Q_INVOKABLE void setModelTemperature(const ModelInfo &m, double t, bool force = false); double modelTopP(const ModelInfo &m) const; @@ -112,7 +132,6 @@ public: bool networkUsageStatsActive() const; void setNetworkUsageStatsActive(bool b); - QVector deviceList() const; void setDeviceList(const QVector &deviceList); diff --git a/gpt4all-chat/qml/ModelDownloaderDialog.qml b/gpt4all-chat/qml/ModelDownloaderDialog.qml index e6e4ce8a..71e7bcdd 100644 --- a/gpt4all-chat/qml/ModelDownloaderDialog.qml +++ b/gpt4all-chat/qml/ModelDownloaderDialog.qml @@ -41,17 +41,222 @@ MyDialog { Label { id: listLabel - text: qsTr("Available Models") - visible: false - Layout.alignment: Qt.AlignLeft + text: qsTr("Discover and Download Models") + visible: true Layout.fillWidth: true + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter color: theme.titleTextColor - font.pixelSize: theme.fontSizeLarge + font.pixelSize: theme.fontSizeLargest font.bold: true } - Item { - height: 0 // for visible space between close button and rest of dialog + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignCenter + Layout.margins: 0 + spacing: 10 + MyTextField { + id: discoverField + property string textBeingSearched: "" + readOnly: ModelList.discoverInProgress + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: 720 + Layout.preferredHeight: 90 + font.pixelSize: theme.fontSizeLarger + placeholderText: qsTr("Discover and download models by keyword search...") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Text field for discovering and filtering downloadable models") + Connections { + target: ModelList + function onDiscoverInProgressChanged() { + if (ModelList.discoverInProgress) { + discoverField.textBeingSearched = discoverField.text; + discoverField.text = qsTr("Searching \u00B7 ") + discoverField.textBeingSearched; + } else { + discoverField.text = discoverField.textBeingSearched; + discoverField.textBeingSearched = ""; + } + } + } + background: ProgressBar { + id: discoverProgressBar + indeterminate: ModelList.discoverInProgress && ModelList.discoverProgress === 0.0 + value: ModelList.discoverProgress + background: Rectangle { + color: theme.controlBackground + radius: 10 + } + contentItem: Item { + Rectangle { + visible: ModelList.discoverInProgress + anchors.bottom: parent.bottom + width: discoverProgressBar.visualPosition * parent.width + height: 10 + radius: 2 + color: theme.progressForeground + } + } + } + + Keys.onReturnPressed: (event)=> { + if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier) + event.accepted = false; + else { + editingFinished(); + sendDiscovery() + } + } + function sendDiscovery() { + ModelList.downloadableModels.discoverAndFilter(discoverField.text); + } + RowLayout { + spacing: 0 + anchors.right: discoverField.right + anchors.verticalCenter: discoverField.verticalCenter + anchors.rightMargin: 15 + visible: !ModelList.discoverInProgress + MyMiniButton { + id: clearDiscoverButton + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + visible: discoverField.text !== "" + contentItem: Text { + color: clearDiscoverButton.hovered ? theme.iconBackgroundDark : theme.textColor + text: "\u2715" + font.pixelSize: theme.fontSizeLarge + } + onClicked: { + discoverField.text = "" + discoverField.sendDiscovery() // should clear results + } + } + MyMiniButton { + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + source: "qrc:/gpt4all/icons/settings.svg" + onClicked: { + discoveryTools.visible = !discoveryTools.visible + } + } + MyMiniButton { + id: sendButton + enabled: !ModelList.discoverInProgress + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + source: "qrc:/gpt4all/icons/send_message.svg" + Accessible.name: qsTr("Initiate model discovery and filtering") + Accessible.description: qsTr("Triggers discovery and filtering of models") + onClicked: { + discoverField.sendDiscovery() + } + } + } + } + } + + RowLayout { + id: discoveryTools + Layout.fillWidth: true + Layout.alignment: Qt.AlignCenter + Layout.margins: 0 + spacing: 20 + visible: false + MyComboBox { + id: comboSort + model: [qsTr("Default"), qsTr("Likes"), qsTr("Downloads"), qsTr("Recent")] + currentIndex: ModelList.discoverSort + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Sort by: ") + comboSort.displayText + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + ModelList.discoverSort = index; + } + } + MyComboBox { + id: comboSortDirection + model: [qsTr("Asc"), qsTr("Desc")] + currentIndex: { + if (ModelList.discoverSortDirection === 1) + return 0 + else + return 1; + } + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Sort dir: ") + comboSortDirection.displayText + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + if (index === 0) + ModelList.discoverSortDirection = 1; + else + ModelList.discoverSortDirection = -1; + } + } + MyComboBox { + id: comboLimit + model: ["5", "10", "20", "50", "100", qsTr("None")] + currentIndex: { + if (ModelList.discoverLimit === 5) + return 0; + else if (ModelList.discoverLimit === 10) + return 1; + else if (ModelList.discoverLimit === 20) + return 2; + else if (ModelList.discoverLimit === 50) + return 3; + else if (ModelList.discoverLimit === 100) + return 4; + else if (ModelList.discoverLimit === -1) + return 5; + } + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Limit: ") + comboLimit.displayText + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + switch (index) { + case 0: + ModelList.discoverLimit = 5; break; + case 1: + ModelList.discoverLimit = 10; break; + case 2: + ModelList.discoverLimit = 20; break; + case 3: + ModelList.discoverLimit = 50; break; + case 4: + ModelList.discoverLimit = 100; break; + case 5: + ModelList.discoverLimit = -1; break; + } + } + } } Label { @@ -213,15 +418,11 @@ MyDialog { Layout.leftMargin: 20 textFormat: Text.StyledText text: "" - + (qsTr("Download size: ") + filesize) - + "
" - + (qsTr("RAM required: ") + (ramrequired > 0 ? ramrequired + " GB" : qsTr("minimal"))) - + "
" - + (qsTr("Parameters: ") + parameters) - + "
" - + (qsTr("Quantization: ") + quant) - + "
" - + (qsTr("Type: ") + type) + + (qsTr("File size: ") + filesize) + + (ramrequired < 0 ? "" : "
" + (qsTr("RAM required: ") + (ramrequired > 0 ? ramrequired + " GB" : qsTr("minimal")))) + + (parameters === "" ? "" : "
" + qsTr("Parameters: ") + parameters) + + (quant === "" ? "" : "
" + (qsTr("Quantization: ") + quant)) + + (type === "" ? "" : "
" + (qsTr("Type: ") + type)) + "
" color: theme.textColor font.pixelSize: theme.fontSizeLarge @@ -350,11 +551,6 @@ MyDialog { Accessible.role: Accessible.EditableText Accessible.name: placeholderText Accessible.description: qsTr("Whether the file hash is being calculated") - TextMetrics { - id: textMetrics - font: apiKey.font - text: apiKey.placeholderText - } } } } diff --git a/gpt4all-chat/qml/ModelSettings.qml b/gpt4all-chat/qml/ModelSettings.qml index 9bfdece6..4616e433 100644 --- a/gpt4all-chat/qml/ModelSettings.qml +++ b/gpt4all-chat/qml/ModelSettings.qml @@ -82,7 +82,7 @@ MySettingsTab { enabled: root.currentModelInfo.isClone text: qsTr("Remove") onClicked: { - ModelList.remove(root.currentModelInfo); + ModelList.removeClone(root.currentModelInfo); comboBox.currentIndex = 0; } } @@ -453,7 +453,7 @@ MySettingsTab { Accessible.description: ToolTip.text } MySettingsLabel { - id: minPLabel + id: minPLabel text: qsTr("Min P") Layout.row: 3 Layout.column: 0 diff --git a/gpt4all-chat/qml/MyMiniButton.qml b/gpt4all-chat/qml/MyMiniButton.qml index d5e5571a..09f981ed 100644 --- a/gpt4all-chat/qml/MyMiniButton.qml +++ b/gpt4all-chat/qml/MyMiniButton.qml @@ -11,8 +11,8 @@ Button { property color backgroundColorHovered: theme.iconBackgroundHovered property alias source: image.source property alias fillMode: image.fillMode - width: 30 - height: 30 + implicitWidth: 30 + implicitHeight: 30 contentItem: Text { text: myButton.text horizontalAlignment: Text.AlignHCenter