import QtCore import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic import QtQuick.Dialogs import QtQuick.Layouts import download import llm import network Dialog { id: modelDownloaderDialog modal: true opacity: 0.9 closePolicy: LLM.chatListModel.currentChat.modelList.length === 0 ? Popup.NoAutoClose : (Popup.CloseOnEscape | Popup.CloseOnPressOutside) background: Rectangle { anchors.fill: parent anchors.margins: -20 color: theme.backgroundDarkest border.width: 1 border.color: theme.dialogBorder radius: 10 } onOpened: { Network.sendModelDownloaderDialog(); } property string defaultModelPath: Download.defaultLocalModelsPath() property alias modelPath: settings.modelPath Settings { id: settings property string modelPath: modelDownloaderDialog.defaultModelPath } Component.onCompleted: { Download.downloadLocalModelsPath = settings.modelPath } Component.onDestruction: { settings.sync() } ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 30 Label { id: listLabel text: "Available Models:" Layout.alignment: Qt.AlignLeft Layout.fillWidth: true color: theme.textColor } ScrollView { id: scrollView ScrollBar.vertical.policy: ScrollBar.AlwaysOn Layout.fillWidth: true Layout.fillHeight: true clip: true ListView { id: modelList model: Download.modelList boundsBehavior: Flickable.StopAtBounds delegate: Item { id: delegateItem width: modelList.width height: modelName.height + modelName.padding + description.height + description.padding objectName: "delegateItem" property bool downloading: false Rectangle { anchors.fill: parent color: index % 2 === 0 ? theme.backgroundLight : theme.backgroundLighter } Text { id: modelName objectName: "modelName" property string filename: modelData.filename text: filename.slice(5, filename.length - 4) padding: 20 anchors.top: parent.top anchors.left: parent.left font.bold: modelData.isDefault || modelData.bestGPTJ || modelData.bestLlama || modelData.bestMPT color: theme.assistantColor Accessible.role: Accessible.Paragraph Accessible.name: qsTr("Model file") Accessible.description: qsTr("Model file to be downloaded") } Text { id: description text: " - " + modelData.description leftPadding: 20 rightPadding: 20 anchors.top: modelName.bottom anchors.left: modelName.left anchors.right: parent.right wrapMode: Text.WordWrap color: theme.textColor Accessible.role: Accessible.Paragraph Accessible.name: qsTr("Description") Accessible.description: qsTr("The description of the file") } Text { id: isDefault text: qsTr("(default)") visible: modelData.isDefault anchors.top: modelName.top anchors.left: modelName.right padding: 20 color: theme.textColor Accessible.role: Accessible.Paragraph Accessible.name: qsTr("Default file") Accessible.description: qsTr("Whether the file is the default model") } Text { text: modelData.filesize anchors.top: modelName.top anchors.left: isDefault.visible ? isDefault.right : modelName.right padding: 20 color: theme.textColor Accessible.role: Accessible.Paragraph Accessible.name: qsTr("File size") Accessible.description: qsTr("The size of the file") } Label { id: speedLabel anchors.top: modelName.top anchors.right: itemProgressBar.left padding: 20 objectName: "speedLabel" color: theme.textColor text: "" visible: downloading Accessible.role: Accessible.Paragraph Accessible.name: qsTr("Download speed") Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second") } ProgressBar { id: itemProgressBar objectName: "itemProgressBar" anchors.top: modelName.top anchors.right: downloadButton.left anchors.topMargin: 20 anchors.rightMargin: 20 width: 100 visible: downloading background: Rectangle { implicitWidth: 200 implicitHeight: 30 color: theme.backgroundDarkest radius: 3 } contentItem: Item { implicitWidth: 200 implicitHeight: 25 Rectangle { width: itemProgressBar.visualPosition * parent.width height: parent.height radius: 2 color: theme.assistantColor } } Accessible.role: Accessible.ProgressBar Accessible.name: qsTr("Download progressBar") Accessible.description: qsTr("Shows the progress made in the download") } Item { visible: modelData.calcHash anchors.top: modelName.top anchors.right: parent.right Label { id: calcHashLabel anchors.right: busyCalcHash.left padding: 20 objectName: "calcHashLabel" color: theme.textColor text: qsTr("Calculating MD5...") Accessible.role: Accessible.Paragraph Accessible.name: text Accessible.description: qsTr("Whether the file hash is being calculated") } BusyIndicator { id: busyCalcHash anchors.right: parent.right padding: 20 running: modelData.calcHash Accessible.role: Accessible.Animation Accessible.name: qsTr("Busy indicator") Accessible.description: qsTr("Displayed when the file hash is being calculated") } } Label { id: installedLabel anchors.top: modelName.top anchors.right: parent.right padding: 20 objectName: "installedLabel" color: theme.textColor text: qsTr("Already installed") visible: modelData.installed Accessible.role: Accessible.Paragraph Accessible.name: text Accessible.description: qsTr("Whether the file is already installed on your system") } Button { id: downloadButton contentItem: Text { color: theme.textColor text: downloading ? "Cancel" : "Download" } anchors.top: modelName.top anchors.right: parent.right anchors.topMargin: 15 anchors.rightMargin: 20 visible: !modelData.installed && !modelData.calcHash onClicked: { if (!downloading) { downloading = true; Download.downloadModel(modelData.filename); } else { downloading = false; Download.cancelDownload(modelData.filename); } } background: Rectangle { opacity: .5 border.color: theme.backgroundLightest border.width: 1 radius: 10 color: theme.backgroundLight } Accessible.role: Accessible.Button Accessible.name: text Accessible.description: qsTr("Cancel/Download button to stop/start the download") } } Component.onCompleted: { Download.downloadProgress.connect(updateProgress); Download.downloadFinished.connect(resetProgress); } property var lastUpdate: ({}) function updateProgress(bytesReceived, bytesTotal, modelName) { let currentTime = new Date().getTime(); for (let i = 0; i < modelList.contentItem.children.length; i++) { let delegateItem = modelList.contentItem.children[i]; if (delegateItem.objectName === "delegateItem") { let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename; if (modelNameText === modelName) { let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar"); progressBar.value = bytesReceived / bytesTotal; // Calculate the download speed if (lastUpdate[modelName] && lastUpdate[modelName].timestamp) { let timeDifference = currentTime - lastUpdate[modelName].timestamp; let bytesDifference = bytesReceived - lastUpdate[modelName].bytesReceived; let speed = (bytesDifference / timeDifference) * 1000; // bytes per second delegateItem.downloading = true // Update the speed label let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel"); if (speed < 1024) { speedLabel.text = speed.toFixed(2) + " B/s"; } else if (speed < 1024 * 1024) { speedLabel.text = (speed / 1024).toFixed(2) + " KB/s"; } else { speedLabel.text = (speed / (1024 * 1024)).toFixed(2) + " MB/s"; } } // Update the lastUpdate object for the current model lastUpdate[modelName] = {"timestamp": currentTime, "bytesReceived": bytesReceived}; break; } } } } function resetProgress(modelName) { for (let i = 0; i < modelList.contentItem.children.length; i++) { let delegateItem = modelList.contentItem.children[i]; if (delegateItem.objectName === "delegateItem") { let modelNameText = delegateItem.children.find(child => child.objectName === "modelName").filename; if (modelNameText === modelName) { let progressBar = delegateItem.children.find(child => child.objectName === "itemProgressBar"); progressBar.value = 0; delegateItem.downloading = false; // Remove speed label text let speedLabel = delegateItem.children.find(child => child.objectName === "speedLabel"); speedLabel.text = ""; // Remove the lastUpdate object for the canceled model delete lastUpdate[modelName]; break; } } } } } } RowLayout { Layout.alignment: Qt.AlignCenter Layout.fillWidth: true spacing: 20 FolderDialog { id: modelPathDialog title: "Please choose a directory" currentFolder: Download.downloadLocalModelsPath onAccepted: { Download.downloadLocalModelsPath = selectedFolder settings.modelPath = Download.downloadLocalModelsPath settings.sync() } } Label { id: modelPathLabel text: qsTr("Download path:") color: theme.textColor Layout.row: 1 Layout.column: 0 } TextField { id: modelPathDisplayLabel text: Download.downloadLocalModelsPath readOnly: true color: theme.textColor Layout.fillWidth: true ToolTip.text: qsTr("Path where model files will be downloaded to") ToolTip.visible: hovered Accessible.role: Accessible.ToolTip Accessible.name: modelPathDisplayLabel.text Accessible.description: ToolTip.text background: Rectangle { color: theme.backgroundLighter radius: 10 } } Button { text: qsTr("Browse") contentItem: Text { text: qsTr("Browse") horizontalAlignment: Text.AlignHCenter color: theme.textColor Accessible.role: Accessible.Button Accessible.name: text Accessible.description: qsTr("Opens a folder picker dialog to choose where to save model files") } background: Rectangle { opacity: .5 border.color: theme.backgroundLightest border.width: 1 radius: 10 color: theme.backgroundLight } onClicked: modelPathDialog.open() } } } }