From 43eef81ca8b99a2dee6d2fb0384d49e6b5f8921f Mon Sep 17 00:00:00 2001 From: Adam Treat Date: Fri, 28 Apr 2023 10:54:05 -0400 Subject: [PATCH] New startup dialog features. --- CMakeLists.txt | 4 +- download.cpp | 112 +++++++++++++++++++- download.h | 27 ++++- main.qml | 57 ++++++++++ network.cpp | 75 ++++++++++--- network.h | 9 +- qml/ModelDownloaderDialog.qml | 2 - qml/NetworkDialog.qml | 1 + qml/NewVersionDialog.qml | 76 +++++++++++++ qml/StartupDialog.qml | 193 ++++++++++++++++++++++++++++++++++ 10 files changed, 530 insertions(+), 26 deletions(-) create mode 100644 qml/NewVersionDialog.qml create mode 100644 qml/StartupDialog.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9732c7fd..e7f589ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,10 +65,12 @@ qt_add_qml_module(chat VERSION 1.0 QML_FILES main.qml - qml/NetworkDialog.qml qml/ModelDownloaderDialog.qml + qml/NetworkDialog.qml + qml/NewVersionDialog.qml qml/ThumbsDownDialog.qml qml/SettingsDialog.qml + qml/StartupDialog.qml qml/PopupDialog.qml qml/Theme.qml RESOURCES diff --git a/download.cpp b/download.cpp index 6b4bf907..6c28c912 100644 --- a/download.cpp +++ b/download.cpp @@ -30,6 +30,7 @@ Download::Download() &Download::handleSslErrors); connect(this, &Download::downloadLocalModelsPathChanged, this, &Download::updateModelList); updateModelList(); + updateReleaseNotes(); QSettings settings; settings.sync(); m_downloadLocalModelsPath = settings.value("modelPath", @@ -40,6 +41,10 @@ bool operator==(const ModelInfo& lhs, const ModelInfo& rhs) { return lhs.filename == rhs.filename && lhs.md5sum == rhs.md5sum; } +bool operator==(const ReleaseInfo& lhs, const ReleaseInfo& rhs) { + return lhs.version == rhs.version; +} + QList Download::modelList() const { // We make sure the default model is listed first @@ -68,6 +73,42 @@ QList Download::modelList() const return values; } +ReleaseInfo Download::releaseInfo() const +{ + const QString currentVersion = QCoreApplication::applicationVersion(); + if (m_releaseMap.contains(currentVersion)) + return m_releaseMap.value(currentVersion); + return ReleaseInfo(); +} + +bool compareVersions(const QString &a, const QString &b) { + QStringList aParts = a.split('.'); + QStringList bParts = b.split('.'); + + for (int i = 0; i < std::min(aParts.size(), bParts.size()); ++i) { + int aInt = aParts[i].toInt(); + int bInt = bParts[i].toInt(); + + if (aInt > bInt) { + return true; + } else if (aInt < bInt) { + return false; + } + } + + return aParts.size() > bParts.size(); +} + +bool Download::hasNewerRelease() const +{ + const QString currentVersion = QCoreApplication::applicationVersion(); + QList versions = m_releaseMap.keys(); + std::sort(versions.begin(), versions.end(), compareVersions); + if (versions.isEmpty()) + return false; + return compareVersions(versions.first(), currentVersion); +} + QString Download::downloadLocalModelsPath() const { return m_downloadLocalModelsPath; } @@ -82,6 +123,17 @@ void Download::setDownloadLocalModelsPath(const QString &modelPath) { } } +bool Download::isFirstStart() const +{ + QSettings settings; + settings.sync(); + QString lastVersionStarted = settings.value("download/lastVersionStarted").toString(); + bool first = lastVersionStarted != QCoreApplication::applicationVersion(); + settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion()); + settings.sync(); + return first; +} + QString Download::defaultLocalModelsPath() const { QString localPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) @@ -117,7 +169,18 @@ void Download::updateModelList() conf.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(conf); QNetworkReply *jsonReply = m_networkManager.get(request); - connect(jsonReply, &QNetworkReply::finished, this, &Download::handleJsonDownloadFinished); + connect(jsonReply, &QNetworkReply::finished, this, &Download::handleModelsJsonDownloadFinished); +} + +void Download::updateReleaseNotes() +{ + QUrl jsonUrl("http://gpt4all.io/meta/release.json"); + QNetworkRequest request(jsonUrl); + QSslConfiguration conf = request.sslConfiguration(); + conf.setPeerVerifyMode(QSslSocket::VerifyNone); + request.setSslConfiguration(conf); + QNetworkReply *jsonReply = m_networkManager.get(request); + connect(jsonReply, &QNetworkReply::finished, this, &Download::handleReleaseJsonDownloadFinished); } void Download::downloadModel(const QString &modelFile) @@ -173,7 +236,7 @@ void Download::handleSslErrors(QNetworkReply *reply, const QList &err qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url; } -void Download::handleJsonDownloadFinished() +void Download::handleModelsJsonDownloadFinished() { #if 0 QByteArray jsonData = QString("" @@ -206,10 +269,10 @@ void Download::handleJsonDownloadFinished() QByteArray jsonData = jsonReply->readAll(); jsonReply->deleteLater(); #endif - parseJsonFile(jsonData); + parseModelsJsonFile(jsonData); } -void Download::parseJsonFile(const QByteArray &jsonData) +void Download::parseModelsJsonFile(const QByteArray &jsonData) { QJsonParseError err; QJsonDocument document = QJsonDocument::fromJson(jsonData, &err); @@ -273,6 +336,47 @@ void Download::parseJsonFile(const QByteArray &jsonData) emit modelListChanged(); } +void Download::handleReleaseJsonDownloadFinished() +{ + QNetworkReply *jsonReply = qobject_cast(sender()); + if (!jsonReply) + return; + + QByteArray jsonData = jsonReply->readAll(); + jsonReply->deleteLater(); + parseReleaseJsonFile(jsonData); +} + +void Download::parseReleaseJsonFile(const QByteArray &jsonData) +{ + QJsonParseError err; + QJsonDocument document = QJsonDocument::fromJson(jsonData, &err); + if (err.error != QJsonParseError::NoError) { + qDebug() << "ERROR: Couldn't parse: " << jsonData << err.errorString(); + return; + } + + QJsonArray jsonArray = document.array(); + + m_releaseMap.clear(); + for (const QJsonValue &value : jsonArray) { + QJsonObject obj = value.toObject(); + + QString version = obj["version"].toString(); + QString notes = obj["notes"].toString(); + QString contributors = obj["contributors"].toString(); + ReleaseInfo releaseInfo; + releaseInfo.version = version; + releaseInfo.notes = notes; + releaseInfo.contributors = contributors; + m_releaseMap.insert(version, releaseInfo); + } + + emit hasNewerReleaseChanged(); + emit releaseInfoChanged(); +} + + void Download::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply *modelReply = qobject_cast(sender()); diff --git a/download.h b/download.h index cf010609..fd63b539 100644 --- a/download.h +++ b/download.h @@ -34,6 +34,18 @@ public: }; Q_DECLARE_METATYPE(ModelInfo) +struct ReleaseInfo { + Q_GADGET + Q_PROPERTY(QString version MEMBER version) + Q_PROPERTY(QString notes MEMBER notes) + Q_PROPERTY(QString contributors MEMBER contributors) + +public: + QString version; + QString notes; + QString contributors; +}; + class HashAndSaveFile : public QObject { Q_OBJECT @@ -56,6 +68,8 @@ class Download : public QObject { Q_OBJECT Q_PROPERTY(QList modelList READ modelList NOTIFY modelListChanged) + Q_PROPERTY(bool hasNewerRelease READ hasNewerRelease NOTIFY hasNewerReleaseChanged) + Q_PROPERTY(ReleaseInfo releaseInfo READ releaseInfo NOTIFY releaseInfoChanged) Q_PROPERTY(QString downloadLocalModelsPath READ downloadLocalModelsPath WRITE setDownloadLocalModelsPath NOTIFY downloadLocalModelsPathChanged) @@ -64,16 +78,21 @@ public: static Download *globalInstance(); QList modelList() const; + ReleaseInfo releaseInfo() const; + bool hasNewerRelease() const; Q_INVOKABLE void updateModelList(); + Q_INVOKABLE void updateReleaseNotes(); Q_INVOKABLE void downloadModel(const QString &modelFile); Q_INVOKABLE void cancelDownload(const QString &modelFile); Q_INVOKABLE QString defaultLocalModelsPath() const; Q_INVOKABLE QString downloadLocalModelsPath() const; Q_INVOKABLE void setDownloadLocalModelsPath(const QString &modelPath); + Q_INVOKABLE bool isFirstStart() const; private Q_SLOTS: void handleSslErrors(QNetworkReply *reply, const QList &errors); - void handleJsonDownloadFinished(); + void handleModelsJsonDownloadFinished(); + void handleReleaseJsonDownloadFinished(); void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); void handleModelDownloadFinished(); void handleHashAndSaveFinished(bool success, @@ -84,15 +103,19 @@ Q_SIGNALS: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal, const QString &modelFile); void downloadFinished(const QString &modelFile); void modelListChanged(); + void releaseInfoChanged(); + void hasNewerReleaseChanged(); void downloadLocalModelsPathChanged(); void requestHashAndSave(const QString &hash, const QString &saveFilePath, QTemporaryFile *tempFile, QNetworkReply *modelReply); private: - void parseJsonFile(const QByteArray &jsonData); + void parseModelsJsonFile(const QByteArray &jsonData); + void parseReleaseJsonFile(const QByteArray &jsonData); HashAndSaveFile *m_hashAndSave; QMap m_modelMap; + QMap m_releaseMap; QNetworkAccessManager m_networkManager; QMap m_activeDownloads; QString m_downloadLocalModelsPath; diff --git a/main.qml b/main.qml index 7c6b35c0..fe344914 100644 --- a/main.qml +++ b/main.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import QtQuick.Controls.Basic import QtQuick.Layouts import llm +import download import network Window { @@ -21,6 +22,62 @@ Window { color: theme.textColor + // Startup code + Component.onCompleted: { + startupDialogs(); + } + + Connections { + target: firstStartDialog + function onClosed() { + startupDialogs(); + } + } + + Connections { + target: downloadNewModels + function onClosed() { + startupDialogs(); + } + } + + Connections { + target: Download + function onHasNewerReleaseChanged() { + startupDialogs(); + } + } + + function startupDialogs() { + // check for first time start of this version + if (Download.isFirstStart()) { + firstStartDialog.open(); + return; + } + + // check for any current models and if not, open download dialog + if (LLM.modelList.length === 0) { + downloadNewModels.open(); + return; + } + + // check for new version + if (Download.hasNewerRelease) { + newVersionDialog.open(); + return; + } + } + + StartupDialog { + id: firstStartDialog + anchors.centerIn: parent + } + + NewVersionDialog { + id: newVersionDialog + anchors.centerIn: parent + } + Item { Accessible.role: Accessible.Window Accessible.name: title diff --git a/network.cpp b/network.cpp index 50c2f321..e613fa36 100644 --- a/network.cpp +++ b/network.cpp @@ -21,17 +21,19 @@ Network *Network::globalInstance() Network::Network() : QObject{nullptr} , m_isActive(false) - , m_isOptIn(false) + , m_usageStatsActive(false) , m_shouldSendStartup(false) { QSettings settings; settings.sync(); - m_isOptIn = settings.value("track", false).toBool(); m_uniqueId = settings.value("uniqueId", generateUniqueId()).toString(); settings.setValue("uniqueId", m_uniqueId); settings.sync(); - setActive(settings.value("network/isActive", false).toBool()); - if (m_isOptIn) + m_isActive = settings.value("network/isActive", false).toBool(); + if (m_isActive) + sendHealth(); + m_usageStatsActive = settings.value("network/usageStatsActive", false).toBool(); + if (m_usageStatsActive) sendIpify(); connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, &Network::handleSslErrors); @@ -50,6 +52,22 @@ void Network::setActive(bool b) sendHealth(); } +void Network::setUsageStatsActive(bool b) +{ + QSettings settings; + settings.setValue("network/usageStatsActive", b); + settings.sync(); + m_usageStatsActive = b; + emit usageStatsActiveChanged(); + if (!m_usageStatsActive) + sendOptOut(); + else { + // model might be loaded already when user opt-in for first time + sendStartup(); + sendIpify(); + } +} + QString Network::generateUniqueId() const { return QUuid::createUuid().toString(QUuid::WithoutBraces); @@ -76,9 +94,9 @@ bool Network::packageAndSendJson(const QString &ingestId, const QString &json) QSettings settings; settings.sync(); - QString attribution = settings.value("attribution", QString()).toString(); + QString attribution = settings.value("network/attribution", QString()).toString(); if (!attribution.isEmpty()) - object.insert("attribution", attribution); + object.insert("network/attribution", attribution); QJsonDocument newDoc; newDoc.setObject(object); @@ -143,23 +161,48 @@ void Network::handleSslErrors(QNetworkReply *reply, const QList &erro qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url; } +void Network::sendOptOut() +{ + QJsonObject properties; + properties.insert("token", "ce362e568ddaee16ed243eaffb5860a2"); + properties.insert("time", QDateTime::currentSecsSinceEpoch()); + properties.insert("distinct_id", m_uniqueId); + properties.insert("$insert_id", generateUniqueId()); + + QJsonObject event; + event.insert("event", "opt_out"); + event.insert("properties", properties); + + QJsonArray array; + array.append(event); + + QJsonDocument doc; + doc.setArray(array); + sendMixpanel(doc.toJson()); + +#if defined(DEBUG) + printf("%s %s\n", qPrintable("opt_out"), qPrintable(doc.toJson(QJsonDocument::Indented))); + fflush(stdout); +#endif +} + void Network::sendModelLoaded() { - if (!m_isOptIn) + if (!m_usageStatsActive) return; sendMixpanelEvent("model_load"); } void Network::sendResetContext() { - if (!m_isOptIn) + if (!m_usageStatsActive) return; sendMixpanelEvent("reset_context"); } void Network::sendStartup() { - if (!m_isOptIn) + if (!m_usageStatsActive) return; m_shouldSendStartup = true; if (m_ipify.isEmpty()) @@ -169,21 +212,21 @@ void Network::sendStartup() void Network::sendShutdown() { - if (!m_isOptIn) + if (!m_usageStatsActive) return; sendMixpanelEvent("shutdown"); } void Network::sendCheckForUpdates() { - if (!m_isOptIn) + if (!m_usageStatsActive) return; sendMixpanelEvent("check_for_updates"); } void Network::sendMixpanelEvent(const QString &ev) { - if (!m_isOptIn) + if (!m_usageStatsActive) return; QJsonObject properties; @@ -217,7 +260,7 @@ void Network::sendMixpanelEvent(const QString &ev) void Network::sendIpify() { - if (!m_isOptIn) + if (!m_usageStatsActive || !m_ipify.isEmpty()) return; QUrl ipifyUrl("https://api.ipify.org"); @@ -231,7 +274,7 @@ void Network::sendIpify() void Network::sendMixpanel(const QByteArray &json) { - if (!m_isOptIn) + if (!m_usageStatsActive) return; QUrl trackUrl("https://api.mixpanel.com/track"); @@ -246,7 +289,7 @@ void Network::sendMixpanel(const QByteArray &json) void Network::handleIpifyFinished() { - Q_ASSERT(m_isOptIn); + Q_ASSERT(m_usageStatsActive); QNetworkReply *reply = qobject_cast(sender()); if (!reply) return; @@ -272,7 +315,7 @@ void Network::handleIpifyFinished() void Network::handleMixpanelFinished() { - Q_ASSERT(m_isOptIn); + Q_ASSERT(m_usageStatsActive); QNetworkReply *reply = qobject_cast(sender()); if (!reply) return; diff --git a/network.h b/network.h index 4aa6c1b8..f57c0a3d 100644 --- a/network.h +++ b/network.h @@ -9,20 +9,27 @@ class Network : public QObject { Q_OBJECT Q_PROPERTY(bool isActive READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool usageStatsActive READ usageStatsActive WRITE setUsageStatsActive NOTIFY usageStatsActiveChanged) + public: static Network *globalInstance(); bool isActive() const { return m_isActive; } void setActive(bool b); + bool usageStatsActive() const { return m_usageStatsActive; } + void setUsageStatsActive(bool b); + Q_INVOKABLE QString generateUniqueId() const; Q_INVOKABLE bool sendConversation(const QString &ingestId, const QString &conversation); Q_SIGNALS: void activeChanged(); + void usageStatsActiveChanged(); void healthCheckFailed(int code); public Q_SLOTS: + void sendOptOut(); void sendModelLoaded(); void sendResetContext(); void sendStartup(); @@ -44,9 +51,9 @@ private: bool packageAndSendJson(const QString &ingestId, const QString &json); private: - bool m_isOptIn; bool m_shouldSendStartup; bool m_isActive; + bool m_usageStatsActive; QString m_ipify; QString m_uniqueId; QNetworkAccessManager m_networkManager; diff --git a/qml/ModelDownloaderDialog.qml b/qml/ModelDownloaderDialog.qml index b1835fa1..c73c007b 100644 --- a/qml/ModelDownloaderDialog.qml +++ b/qml/ModelDownloaderDialog.qml @@ -32,8 +32,6 @@ Dialog { Component.onCompleted: { Download.downloadLocalModelsPath = settings.modelPath - if (LLM.modelList.length === 0) - open(); } Component.onDestruction: { diff --git a/qml/NetworkDialog.qml b/qml/NetworkDialog.qml index ba93163e..1d1a4dad 100644 --- a/qml/NetworkDialog.qml +++ b/qml/NetworkDialog.qml @@ -22,6 +22,7 @@ Dialog { Settings { id: settings + category: "network" property string attribution: "" } diff --git a/qml/NewVersionDialog.qml b/qml/NewVersionDialog.qml new file mode 100644 index 00000000..8da15f31 --- /dev/null +++ b/qml/NewVersionDialog.qml @@ -0,0 +1,76 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts +import download +import network +import llm + +Dialog { + id: newVerionDialog + anchors.centerIn: parent + modal: true + opacity: 0.9 + width: contentItem.width + height: contentItem.height + padding: 20 + + Theme { + id: theme + } + + background: Rectangle { + anchors.fill: parent + color: theme.backgroundDarkest + border.width: 1 + border.color: theme.dialogBorder + radius: 10 + } + + Item { + id: contentItem + width: childrenRect.width + 40 + height: childrenRect.height + 40 + + Label { + id: label + anchors.top: parent.top + anchors.left: parent.left + topPadding: 20 + bottomPadding: 20 + text: qsTr("New version is available:") + color: theme.textColor + } + + Button { + id: button + anchors.left: label.right + anchors.leftMargin: 10 + anchors.verticalCenter: label.verticalCenter + padding: 20 + contentItem: Text { + text: qsTr("Update") + horizontalAlignment: Text.AlignHCenter + color: theme.textColor + + Accessible.role: Accessible.Button + Accessible.name: text + Accessible.description: qsTr("Use this to launch an external application that will check for updates to the installer") + } + + background: Rectangle { + opacity: .5 + border.color: theme.backgroundLightest + border.width: 1 + radius: 10 + color: theme.backgroundLight + } + + onClicked: { + if (!LLM.checkForUpdates()) + checkForUpdatesError.open() + } + } + } +} diff --git a/qml/StartupDialog.qml b/qml/StartupDialog.qml new file mode 100644 index 00000000..da676cf8 --- /dev/null +++ b/qml/StartupDialog.qml @@ -0,0 +1,193 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts +import download +import network +import llm + +Dialog { + id: startupDialog + anchors.centerIn: parent + modal: false + opacity: 0.9 + padding: 20 + width: 1024 + height: column.height + 40 + + Theme { + id: theme + } + + Connections { + target: startupDialog + function onClosed() { + if (!Network.usageStatsActive) + Network.usageStatsActive = false // opt-out triggered + } + } + + Column { + id: column + spacing: 20 + Item { + width: childrenRect.width + height: childrenRect.height + Image { + id: img + anchors.top: parent.top + anchors.left: parent.left + width: 60 + height: 60 + source: "qrc:/gpt4all/icons/logo.svg" + } + Text { + anchors.left: img.right + anchors.leftMargin: 30 + anchors.verticalCenter: img.verticalCenter + text: qsTr("Welcome!") + color: theme.textColor + } + } + + ScrollView { + clip: true + height: 200 + width: 1024 - 40 + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + TextArea { + id: welcome + wrapMode: Text.Wrap + width: 1024 - 40 + padding: 20 + textFormat: TextEdit.MarkdownText + text: qsTr("### Release notes\n") + + Download.releaseInfo.notes + + qsTr("### Contributors\n") + + Download.releaseInfo.contributors + color: theme.textColor + focus: false + readOnly: true + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Release notes") + Accessible.description: qsTr("Release notes for this version") + background: Rectangle { + color: theme.backgroundLight + radius: 10 + } + } + } + + ScrollView { + clip: true + height: 150 + width: 1024 - 40 + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + TextArea { + id: optInTerms + wrapMode: Text.Wrap + width: 1024 - 40 + padding: 20 + textFormat: TextEdit.MarkdownText + text: qsTr( +"### Opt-ins for anonymous usage analytics and datalake +By enabling these features, you will be able to participate in the democratic process of training a +large language model by contributing data for future model improvements. + +When a GPT4All model responds to you and you have opted-in, you can like/dislike its response. If you +dislike a response, you can suggest an alternative response. This data will be collected and aggregated +in the GPT4All Datalake. + +NOTE: By turning on this feature, you will be sending your data to the GPT4All Open Source Datalake. +You should have no expectation of chat privacy when this feature is enabled. You should; however, have +an expectation of an optional attribution if you wish. Your chat data will be openly available for anyone +to download and will be used by Nomic AI to improve future GPT4All models. Nomic AI will retain all +attribution information attached to your data and you will be credited as a contributor to any GPT4All +model release that uses your data!") + + color: theme.textColor + focus: false + readOnly: true + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Terms for opt-in") + Accessible.description: qsTr("Describes what will happen when you opt-in") + background: Rectangle { + color: theme.backgroundLight + radius: 10 + } + } + } + + GridLayout { + columns: 2 + rowSpacing: 10 + columnSpacing: 10 + anchors.right: parent.right + Label { + id: optInStatistics + text: "Opt-in to anonymous usage analytics used to improve GPT4All" + Layout.row: 0 + Layout.column: 0 + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Opt-in for anonymous usage statistics") + Accessible.description: qsTr("Label for opt-in") + } + + CheckBox { + id: optInStatisticsBox + Layout.alignment: Qt.AlignVCenter + Layout.row: 0 + Layout.column: 1 + property bool defaultChecked: Network.usageStatsActive + checked: defaultChecked + Accessible.role: Accessible.CheckBox + Accessible.name: qsTr("Opt-in for anonymous usage statistics") + Accessible.description: qsTr("Checkbox to allow opt-in for anonymous usage statistics") + onClicked: { + Network.usageStatsActive = optInStatisticsBox.checked + if (optInNetworkBox.checked && optInStatisticsBox.checked) + startupDialog.close() + } + } + Label { + id: optInNetwork + text: "Opt-in to anonymous sharing of chats to the GPT4All Datalake" + Layout.row: 1 + Layout.column: 0 + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Opt-in for network") + Accessible.description: qsTr("Checkbox to allow opt-in for network") + } + + CheckBox { + id: optInNetworkBox + Layout.alignment: Qt.AlignVCenter + Layout.row: 1 + Layout.column: 1 + property bool defaultChecked: Network.isActive + checked: defaultChecked + Accessible.role: Accessible.CheckBox + Accessible.name: qsTr("Opt-in for network") + Accessible.description: qsTr("Label for opt-in") + onClicked: { + Network.isActive = optInNetworkBox.checked + if (optInNetworkBox.checked && optInStatisticsBox.checked) + startupDialog.close() + } + } + } + } + + background: Rectangle { + anchors.fill: parent + color: theme.backgroundDarkest + border.width: 1 + border.color: theme.dialogBorder + radius: 10 + } +}