New startup dialog features.

pull/520/head
Adam Treat 1 year ago
parent f8754cbe1b
commit 43eef81ca8

@ -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

@ -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<ModelInfo> Download::modelList() const
{
// We make sure the default model is listed first
@ -68,6 +73,42 @@ QList<ModelInfo> 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<QString> 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<QSslError> &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<QNetworkReply *>(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<QNetworkReply *>(sender());

@ -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<ModelInfo> 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<ModelInfo> 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<QSslError> &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<QString, ModelInfo> m_modelMap;
QMap<QString, ReleaseInfo> m_releaseMap;
QNetworkAccessManager m_networkManager;
QMap<QNetworkReply*, QTemporaryFile*> m_activeDownloads;
QString m_downloadLocalModelsPath;

@ -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

@ -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<QSslError> &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<QNetworkReply *>(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<QNetworkReply *>(sender());
if (!reply)
return;

@ -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;

@ -32,8 +32,6 @@ Dialog {
Component.onCompleted: {
Download.downloadLocalModelsPath = settings.modelPath
if (LLM.modelList.length === 0)
open();
}
Component.onDestruction: {

@ -22,6 +22,7 @@ Dialog {
Settings {
id: settings
category: "network"
property string attribution: ""
}

@ -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()
}
}
}
}

@ -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
}
}
Loading…
Cancel
Save