mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2026-07-17 10:58:08 +00:00
Compare commits
22 Commits
v3.0.0-rc1
...
v3.0.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d92252cab1 | ||
|
|
6506ba161b | ||
|
|
bed92046d0 | ||
|
|
a1ec6f2150 | ||
|
|
8d6e11fcad | ||
|
|
fc5dc9dd1a | ||
|
|
d4494602e2 | ||
|
|
23e8f43c5a | ||
|
|
31fa575c35 | ||
|
|
6d593d6ea1 | ||
|
|
01870b4a46 | ||
|
|
53fc2d56f6 | ||
|
|
e5d9936d04 | ||
|
|
11823022e2 | ||
|
|
da1823ed7a | ||
|
|
3a61070f82 | ||
|
|
c87ccf4124 | ||
|
|
88f5face2b | ||
|
|
f8a935d8a6 | ||
|
|
029bd318e9 | ||
|
|
d5968f4ab2 | ||
|
|
30febbe3d2 |
@@ -77,8 +77,90 @@ jobs:
|
||||
~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target package
|
||||
mkdir upload
|
||||
cp gpt4all-installer-* upload
|
||||
# persist the unsigned installer
|
||||
- store_artifacts:
|
||||
path: build/upload
|
||||
# add workspace so signing jobs can connect & obtain dmg
|
||||
- persist_to_workspace:
|
||||
root: build
|
||||
# specify path to only include components we want to persist
|
||||
# accross builds
|
||||
paths:
|
||||
- upload
|
||||
|
||||
sign-offline-chat-installer-macos:
|
||||
macos:
|
||||
xcode: 14.0.0
|
||||
steps:
|
||||
- checkout
|
||||
# attach to a workspace containing unsigned dmg
|
||||
- attach_workspace:
|
||||
at: build
|
||||
- run:
|
||||
name: "Setup Keychain"
|
||||
command: |
|
||||
echo $MAC_SIGNING_CERT | base64 --decode > cert.p12
|
||||
# cat \<<< "$MAC_SIGNING_CERT" > certs1.pem
|
||||
# file certs1.pem
|
||||
# iconv -c -f UTF8 -t ASCII certs1.pem > certs.pem
|
||||
# openssl pkcs12 -legacy -export -out cert.p12 -in certs.pem -inkey certs.pem -passin pass:"$MAC_SIGNING_CERT_PWD" -passout pass:"$MAC_SIGNING_CERT_PWD"
|
||||
security create-keychain -p "$MAC_KEYCHAIN_KEY" sign.keychain
|
||||
security default-keychain -s sign.keychain
|
||||
security unlock-keychain -p "$MAC_KEYCHAIN_KEY" sign.keychain
|
||||
security import cert.p12 -k sign.keychain -P "$MAC_SIGNING_CERT_PWD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_KEYCHAIN_KEY" sign.keychain
|
||||
rm cert.p12
|
||||
- run:
|
||||
name: "Sign App Bundle"
|
||||
command: |
|
||||
python3 -m pip install click
|
||||
python3 gpt4all-chat/cmake/sign_dmg.py --input-dmg build/upload/gpt4all-installer-darwin.dmg --output-dmg build/upload/gpt4all-installer-darwin-signed.dmg --signing-identity "$MAC_SIGNING_CERT_NAME"
|
||||
- run:
|
||||
name: "Sign DMG"
|
||||
command: |
|
||||
codesign --options runtime --timestamp -s "$MAC_SIGNING_CERT_NAME" build/upload/gpt4all-installer-darwin-signed.dmg
|
||||
# add workspace so signing jobs can connect & obtain dmg
|
||||
- persist_to_workspace:
|
||||
root: build
|
||||
# specify path to only include components we want to persist
|
||||
# accross builds
|
||||
paths:
|
||||
- upload
|
||||
|
||||
notarize-offline-chat-installer-macos:
|
||||
macos:
|
||||
xcode: 14.0.0
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: build
|
||||
# - run:
|
||||
# name: "Setup Notarize Keychain"
|
||||
# command: |
|
||||
# security create-keychain
|
||||
# sudo xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MAC_NOTARIZATION_ID" --team-id "$MAC_NOTARIZATION_TID" --password "$MAC_NOTARIZATION_KEY" --keychain /Library/Keychains/System.keychain
|
||||
- run:
|
||||
name: "Notarize"
|
||||
command: |
|
||||
xcrun notarytool submit build/upload/gpt4all-installer-darwin-signed.dmg --apple-id "$MAC_NOTARIZATION_ID" --team-id "$MAC_NOTARIZATION_TID" --password "$MAC_NOTARIZATION_KEY" --wait | tee notarize_log.txt
|
||||
- run:
|
||||
name: "Report Notarization Failure"
|
||||
command: |
|
||||
NID=`python3 .circleci/grab_notary_id.py notarize_log.txt` && export NID
|
||||
xcrun notarytool log $NID --keychain-profile "notary-profile"
|
||||
exit 1
|
||||
when: on_fail
|
||||
# - run:
|
||||
# name: "Rename and move"
|
||||
# command: |
|
||||
# mv build/upload/gpt4all-installer-darwin-signed.dmg build/upload-signed/gpt4all-installer-darwin-signed.dmg
|
||||
- run:
|
||||
name: "Staple"
|
||||
command: |
|
||||
xcrun stapler staple build/upload/gpt4all-installer-darwin-signed.dmg
|
||||
- store_artifacts:
|
||||
path: build/upload
|
||||
|
||||
build-offline-chat-installer-linux:
|
||||
machine:
|
||||
image: ubuntu-2204:2023.04.2
|
||||
@@ -848,6 +930,12 @@ workflows:
|
||||
- build-offline-chat-installer-macos:
|
||||
requires:
|
||||
- hold
|
||||
- sign-offline-chat-installer-macos:
|
||||
requires:
|
||||
- build-offline-chat-installer-macos
|
||||
- notarize-offline-chat-installer-macos:
|
||||
requires:
|
||||
- sign-offline-chat-installer-macos
|
||||
- build-offline-chat-installer-windows:
|
||||
requires:
|
||||
- hold
|
||||
|
||||
17
.circleci/grab_notary_id.py
Normal file
17
.circleci/grab_notary_id.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
ID_REG = r"id: (.*)"
|
||||
|
||||
def main() -> None:
|
||||
notary_log = sys.argv[1]
|
||||
with open(notary_log, "r") as f:
|
||||
notary_output = f.read()
|
||||
id_m = re.search(ID_REG, notary_output)
|
||||
if id_m:
|
||||
print(id_m.group(1))
|
||||
else:
|
||||
raise RuntimeError("Unable to parse ID from notarization logs")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -65,6 +65,10 @@ if (LLMODEL_VULKAN)
|
||||
list(APPEND BUILD_VARIANTS vulkan vulkan-avxonly)
|
||||
endif()
|
||||
if (LLMODEL_CUDA)
|
||||
if (DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(GGML_CUDA_ARCHITECTURES "${CMAKE_CUDA_ARCHITECTURES}")
|
||||
endif()
|
||||
|
||||
include(CheckLanguage)
|
||||
check_language(CUDA)
|
||||
if (NOT CMAKE_CUDA_COMPILER)
|
||||
|
||||
Submodule gpt4all-backend/llama.cpp-mainline updated: f67f4651fa...b2db03acf2
@@ -371,6 +371,20 @@ function(include_ggml SUFFIX)
|
||||
find_package(CUDAToolkit REQUIRED)
|
||||
set(CUDAToolkit_BIN_DIR ${CUDAToolkit_BIN_DIR} PARENT_SCOPE)
|
||||
|
||||
if (NOT DEFINED GGML_CUDA_ARCHITECTURES)
|
||||
# 52 == lowest CUDA 12 standard
|
||||
# 60 == f16 CUDA intrinsics
|
||||
# 61 == integer CUDA intrinsics
|
||||
# 70 == compute capability at which unrolling a loop in mul_mat_q kernels is faster
|
||||
if (LLAMA_CUDA_F16 OR LLAMA_CUDA_DMMV_F16)
|
||||
set(GGML_CUDA_ARCHITECTURES "60;61;70") # needed for f16 CUDA intrinsics
|
||||
else()
|
||||
set(GGML_CUDA_ARCHITECTURES "52;61;70") # lowest CUDA 12 standard + lowest for integer intrinsics
|
||||
#set(GGML_CUDA_ARCHITECTURES "OFF") # use this to compile much faster, but only F16 models work
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Using CUDA architectures: ${GGML_CUDA_ARCHITECTURES}")
|
||||
|
||||
set(GGML_HEADERS_CUDA ${DIRECTORY}/ggml-cuda.h)
|
||||
|
||||
file(GLOB GGML_SOURCES_CUDA "${DIRECTORY}/ggml-cuda/*.cu")
|
||||
@@ -406,22 +420,6 @@ function(include_ggml SUFFIX)
|
||||
endif()
|
||||
|
||||
set(LLAMA_EXTRA_LIBS ${LLAMA_EXTRA_LIBS} CUDA::cuda_driver)
|
||||
|
||||
if (DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(GGML_CUDA_ARCHITECTURES "${CMAKE_CUDA_ARCHITECTURES}")
|
||||
else()
|
||||
# 52 == lowest CUDA 12 standard
|
||||
# 60 == f16 CUDA intrinsics
|
||||
# 61 == integer CUDA intrinsics
|
||||
# 70 == compute capability at which unrolling a loop in mul_mat_q kernels is faster
|
||||
if (LLAMA_CUDA_F16 OR LLAMA_CUDA_DMMV_F16)
|
||||
set(GGML_CUDA_ARCHITECTURES "60;61;70") # needed for f16 CUDA intrinsics
|
||||
else()
|
||||
set(GGML_CUDA_ARCHITECTURES "52;61;70") # lowest CUDA 12 standard + lowest for integer intrinsics
|
||||
#set(GGML_CUDA_ARCHITECTURES "") # use this to compile much faster, but only F16 models work
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Using CUDA architectures: ${GGML_CUDA_ARCHITECTURES}")
|
||||
endif()
|
||||
|
||||
if (LLAMA_CLBLAST)
|
||||
|
||||
@@ -371,6 +371,11 @@ bool LLamaModel::loadModel(const std::string &modelPath, int n_ctx, int ngl)
|
||||
d_ptr->model_params.main_gpu = d_ptr->device;
|
||||
d_ptr->model_params.n_gpu_layers = ngl;
|
||||
d_ptr->model_params.split_mode = LLAMA_SPLIT_MODE_NONE;
|
||||
} else {
|
||||
#ifdef GGML_USE_CUDA
|
||||
std::cerr << "Llama ERROR: CUDA loadModel was called without a device\n";
|
||||
return false;
|
||||
#endif // GGML_USE_CUDA
|
||||
}
|
||||
#elif defined(GGML_USE_METAL)
|
||||
(void)ngl;
|
||||
@@ -383,15 +388,17 @@ bool LLamaModel::loadModel(const std::string &modelPath, int n_ctx, int ngl)
|
||||
// always fully offload on Metal
|
||||
// TODO(cebtenzzre): use this parameter to allow using more than 53% of system RAM to load a model
|
||||
d_ptr->model_params.n_gpu_layers = 100;
|
||||
#else
|
||||
#else // !KOMPUTE && !VULKAN && !CUDA && !METAL
|
||||
(void)ngl;
|
||||
#endif
|
||||
|
||||
d_ptr->model = llama_load_model_from_file_gpt4all(modelPath.c_str(), &d_ptr->model_params);
|
||||
d_ptr->model = llama_load_model_from_file(modelPath.c_str(), d_ptr->model_params);
|
||||
if (!d_ptr->model) {
|
||||
fflush(stdout);
|
||||
#ifndef GGML_USE_CUDA
|
||||
d_ptr->device = -1;
|
||||
d_ptr->deviceName.clear();
|
||||
#endif
|
||||
std::cerr << "LLAMA ERROR: failed to load model from " << modelPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
@@ -434,8 +441,10 @@ bool LLamaModel::loadModel(const std::string &modelPath, int n_ctx, int ngl)
|
||||
std::cerr << "LLAMA ERROR: failed to init context for model " << modelPath << std::endl;
|
||||
llama_free_model(d_ptr->model);
|
||||
d_ptr->model = nullptr;
|
||||
#ifndef GGML_USE_CUDA
|
||||
d_ptr->device = -1;
|
||||
d_ptr->deviceName.clear();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -723,31 +732,16 @@ bool LLamaModel::initializeGPUDevice(int device, std::string *unavail_reason) co
|
||||
#endif
|
||||
}
|
||||
|
||||
bool LLamaModel::hasGPUDevice() const
|
||||
{
|
||||
#if defined(GGML_USE_KOMPUTE) || defined(GGML_USE_VULKAN) || defined(GGML_USE_CUDA)
|
||||
return d_ptr->device != -1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool LLamaModel::usingGPUDevice() const
|
||||
{
|
||||
bool hasDevice;
|
||||
if (!d_ptr->model)
|
||||
return false;
|
||||
|
||||
bool usingGPU = llama_model_using_gpu(d_ptr->model);
|
||||
#ifdef GGML_USE_KOMPUTE
|
||||
hasDevice = hasGPUDevice() && d_ptr->model_params.n_gpu_layers > 0;
|
||||
assert(!hasDevice || ggml_vk_has_device());
|
||||
#elif defined(GGML_USE_VULKAN) || defined(GGML_USE_CUDA)
|
||||
hasDevice = hasGPUDevice() && d_ptr->model_params.n_gpu_layers > 0;
|
||||
#elif defined(GGML_USE_METAL)
|
||||
hasDevice = true;
|
||||
#else
|
||||
hasDevice = false;
|
||||
assert(!usingGPU || ggml_vk_has_device());
|
||||
#endif
|
||||
|
||||
return hasDevice;
|
||||
return usingGPU;
|
||||
}
|
||||
|
||||
const char *LLamaModel::backendName() const
|
||||
@@ -760,6 +754,8 @@ const char *LLamaModel::gpuDeviceName() const
|
||||
if (usingGPUDevice()) {
|
||||
#if defined(GGML_USE_KOMPUTE) || defined(GGML_USE_VULKAN) || defined(GGML_USE_CUDA)
|
||||
return d_ptr->deviceName.c_str();
|
||||
#elif defined(GGML_USE_METAL)
|
||||
return "Metal";
|
||||
#endif
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
@@ -34,7 +34,6 @@ public:
|
||||
std::vector<GPUDevice> availableGPUDevices(size_t memoryRequired = 0) const override;
|
||||
bool initializeGPUDevice(size_t memoryRequired, const std::string &name) const override;
|
||||
bool initializeGPUDevice(int device, std::string *unavail_reason = nullptr) const override;
|
||||
bool hasGPUDevice() const override;
|
||||
bool usingGPUDevice() const override;
|
||||
const char *backendName() const override;
|
||||
const char *gpuDeviceName() const override;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define LLMODEL_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -57,23 +58,30 @@ public:
|
||||
backend(backend), index(index), type(type), heapSize(heapSize), name(std::move(name)),
|
||||
vendor(std::move(vendor)) {}
|
||||
|
||||
std::string selectionName() const { return m_backendNames.at(backend) + ": " + name; }
|
||||
std::string reportedName() const { return name + " (" + m_backendNames.at(backend) + ")"; }
|
||||
std::string selectionName() const
|
||||
{
|
||||
assert(backend == "cuda"s || backend == "kompute"s);
|
||||
return backendName() + ": " + name;
|
||||
}
|
||||
|
||||
std::string backendName() const { return backendIdToName(backend); }
|
||||
|
||||
static std::string backendIdToName(const std::string &backend) { return s_backendNames.at(backend); }
|
||||
|
||||
static std::string updateSelectionName(const std::string &name) {
|
||||
if (name == "Auto" || name == "CPU" || name == "Metal")
|
||||
return name;
|
||||
auto it = std::find_if(m_backendNames.begin(), m_backendNames.end(), [&name](const auto &entry) {
|
||||
auto it = std::find_if(s_backendNames.begin(), s_backendNames.end(), [&name](const auto &entry) {
|
||||
return name.starts_with(entry.second + ": ");
|
||||
});
|
||||
if (it != m_backendNames.end())
|
||||
if (it != s_backendNames.end())
|
||||
return name;
|
||||
return "Vulkan: " + name; // previously, there were only Vulkan devices
|
||||
}
|
||||
|
||||
private:
|
||||
static inline const std::unordered_map<std::string, std::string> m_backendNames {
|
||||
{"cuda", "CUDA"}, {"kompute", "Vulkan"},
|
||||
static inline const std::unordered_map<std::string, std::string> s_backendNames {
|
||||
{"cpu", "CPU"}, {"metal", "Metal"}, {"cuda", "CUDA"}, {"kompute", "Vulkan"},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -196,7 +204,6 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool hasGPUDevice() const { return false; }
|
||||
virtual bool usingGPUDevice() const { return false; }
|
||||
virtual const char *backendName() const { return "cpu"; }
|
||||
virtual const char *gpuDeviceName() const { return nullptr; }
|
||||
|
||||
@@ -287,12 +287,6 @@ bool llmodel_gpu_init_gpu_device_by_int(llmodel_model model, int device)
|
||||
return wrapper->llModel->initializeGPUDevice(device);
|
||||
}
|
||||
|
||||
bool llmodel_has_gpu_device(llmodel_model model)
|
||||
{
|
||||
const auto *wrapper = static_cast<LLModelWrapper *>(model);
|
||||
return wrapper->llModel->hasGPUDevice();
|
||||
}
|
||||
|
||||
const char *llmodel_model_backend_name(llmodel_model model)
|
||||
{
|
||||
const auto *wrapper = static_cast<LLModelWrapper *>(model);
|
||||
|
||||
@@ -291,11 +291,6 @@ bool llmodel_gpu_init_gpu_device_by_struct(llmodel_model model, const llmodel_gp
|
||||
*/
|
||||
bool llmodel_gpu_init_gpu_device_by_int(llmodel_model model, int device);
|
||||
|
||||
/**
|
||||
* @return True if a GPU device is successfully initialized, false otherwise.
|
||||
*/
|
||||
bool llmodel_has_gpu_device(llmodel_model model);
|
||||
|
||||
/**
|
||||
* @return The name of the llama.cpp backend currently in use. One of "cpu", "kompute", or "metal".
|
||||
*/
|
||||
|
||||
@@ -177,9 +177,6 @@ llmodel.llmodel_gpu_init_gpu_device_by_struct.restype = ctypes.c_bool
|
||||
llmodel.llmodel_gpu_init_gpu_device_by_int.argtypes = [ctypes.c_void_p, ctypes.c_int32]
|
||||
llmodel.llmodel_gpu_init_gpu_device_by_int.restype = ctypes.c_bool
|
||||
|
||||
llmodel.llmodel_has_gpu_device.argtypes = [ctypes.c_void_p]
|
||||
llmodel.llmodel_has_gpu_device.restype = ctypes.c_bool
|
||||
|
||||
llmodel.llmodel_model_backend_name.argtypes = [ctypes.c_void_p]
|
||||
llmodel.llmodel_model_backend_name.restype = ctypes.c_char_p
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ set(APP_VERSION_MAJOR 3)
|
||||
set(APP_VERSION_MINOR 0)
|
||||
set(APP_VERSION_PATCH 0)
|
||||
set(APP_VERSION_BASE "${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_PATCH}")
|
||||
set(APP_VERSION "${APP_VERSION_BASE}-rc1")
|
||||
set(APP_VERSION "${APP_VERSION_BASE}-rc2")
|
||||
|
||||
# Include the binary directory for the generated header file
|
||||
include_directories("${CMAKE_CURRENT_BINARY_DIR}")
|
||||
@@ -174,6 +174,8 @@ qt_add_qml_module(chat
|
||||
icons/send_message.svg
|
||||
icons/stop_generating.svg
|
||||
icons/regenerate.svg
|
||||
icons/caret_down.svg
|
||||
icons/caret_right.svg
|
||||
icons/chat.svg
|
||||
icons/changelog.svg
|
||||
icons/close.svg
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2023 Nomic, Inc., Aaron Miller
|
||||
Copyright 2023-2024 Nomic, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -64,8 +64,7 @@ void Chat::connectLLM()
|
||||
connect(m_llmodel, &ChatLLM::recalcChanged, this, &Chat::handleRecalculating, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::generatedNameChanged, this, &Chat::generatedNameChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::reportSpeed, this, &Chat::handleTokenSpeedChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::reportDevice, this, &Chat::handleDeviceChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::reportFallbackReason, this, &Chat::handleFallbackReasonChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::loadedModelInfoChanged, this, &Chat::loadedModelInfoChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::databaseResultsChanged, this, &Chat::handleDatabaseResultsChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::modelInfoChanged, this, &Chat::handleModelInfoChanged, Qt::QueuedConnection);
|
||||
connect(m_llmodel, &ChatLLM::trySwitchContextOfLoadedModelCompleted, this, &Chat::handleTrySwitchContextOfLoadedModelCompleted, Qt::QueuedConnection);
|
||||
@@ -327,16 +326,19 @@ void Chat::handleTokenSpeedChanged(const QString &tokenSpeed)
|
||||
emit tokenSpeedChanged();
|
||||
}
|
||||
|
||||
void Chat::handleDeviceChanged(const QString &device)
|
||||
QString Chat::deviceBackend() const
|
||||
{
|
||||
m_device = device;
|
||||
emit deviceChanged();
|
||||
return m_llmodel->deviceBackend();
|
||||
}
|
||||
|
||||
void Chat::handleFallbackReasonChanged(const QString &fallbackReason)
|
||||
QString Chat::device() const
|
||||
{
|
||||
m_fallbackReason = fallbackReason;
|
||||
emit fallbackReasonChanged();
|
||||
return m_llmodel->device();
|
||||
}
|
||||
|
||||
QString Chat::fallbackReason() const
|
||||
{
|
||||
return m_llmodel->fallbackReason();
|
||||
}
|
||||
|
||||
void Chat::handleDatabaseResultsChanged(const QList<ResultInfo> &results)
|
||||
|
||||
@@ -33,8 +33,9 @@ class Chat : public QObject
|
||||
Q_PROPERTY(QList<QString> collectionList READ collectionList NOTIFY collectionListChanged)
|
||||
Q_PROPERTY(QString modelLoadingError READ modelLoadingError NOTIFY modelLoadingErrorChanged)
|
||||
Q_PROPERTY(QString tokenSpeed READ tokenSpeed NOTIFY tokenSpeedChanged);
|
||||
Q_PROPERTY(QString device READ device NOTIFY deviceChanged);
|
||||
Q_PROPERTY(QString fallbackReason READ fallbackReason NOTIFY fallbackReasonChanged);
|
||||
Q_PROPERTY(QString deviceBackend READ deviceBackend NOTIFY loadedModelInfoChanged)
|
||||
Q_PROPERTY(QString device READ device NOTIFY loadedModelInfoChanged)
|
||||
Q_PROPERTY(QString fallbackReason READ fallbackReason NOTIFY loadedModelInfoChanged)
|
||||
Q_PROPERTY(LocalDocsCollectionsModel *collectionModel READ collectionModel NOTIFY collectionModelChanged)
|
||||
// 0=no, 1=waiting, 2=working
|
||||
Q_PROPERTY(int trySwitchContextInProgress READ trySwitchContextInProgress NOTIFY trySwitchContextInProgressChanged)
|
||||
@@ -111,8 +112,10 @@ public:
|
||||
QString modelLoadingError() const { return m_modelLoadingError; }
|
||||
|
||||
QString tokenSpeed() const { return m_tokenSpeed; }
|
||||
QString device() const { return m_device; }
|
||||
QString fallbackReason() const { return m_fallbackReason; }
|
||||
QString deviceBackend() const;
|
||||
QString device() const;
|
||||
// not loaded -> QString(), no fallback -> QString("")
|
||||
QString fallbackReason() const;
|
||||
|
||||
int trySwitchContextInProgress() const { return m_trySwitchContextInProgress; }
|
||||
|
||||
@@ -149,6 +152,7 @@ Q_SIGNALS:
|
||||
void fallbackReasonChanged();
|
||||
void collectionModelChanged();
|
||||
void trySwitchContextInProgressChanged();
|
||||
void loadedModelInfoChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void handleResponseChanged(const QString &response);
|
||||
@@ -159,8 +163,6 @@ private Q_SLOTS:
|
||||
void handleRecalculating();
|
||||
void handleModelLoadingError(const QString &error);
|
||||
void handleTokenSpeedChanged(const QString &tokenSpeed);
|
||||
void handleDeviceChanged(const QString &device);
|
||||
void handleFallbackReasonChanged(const QString &device);
|
||||
void handleDatabaseResultsChanged(const QList<ResultInfo> &results);
|
||||
void handleModelInfoChanged(const ModelInfo &modelInfo);
|
||||
void handleTrySwitchContextOfLoadedModelCompleted(int value);
|
||||
|
||||
@@ -93,6 +93,12 @@ void LLModelStore::destroy()
|
||||
m_availableModel.reset();
|
||||
}
|
||||
|
||||
void LLModelInfo::resetModel(ChatLLM *cllm, LLModel *model) {
|
||||
this->model.reset(model);
|
||||
fallbackReason.reset();
|
||||
emit cllm->loadedModelInfoChanged();
|
||||
}
|
||||
|
||||
ChatLLM::ChatLLM(Chat *parent, bool isServer)
|
||||
: QObject{nullptr}
|
||||
, m_promptResponseTokens(0)
|
||||
@@ -141,7 +147,7 @@ void ChatLLM::destroy()
|
||||
// The only time we should have a model loaded here is on shutdown
|
||||
// as we explicitly unload the model in all other circumstances
|
||||
if (isModelLoaded()) {
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +214,7 @@ void ChatLLM::trySwitchContextOfLoadedModel(const ModelInfo &modelInfo)
|
||||
QString filePath = modelInfo.dirpath + modelInfo.filename();
|
||||
QFileInfo fileInfo(filePath);
|
||||
|
||||
m_llModelInfo = LLModelStore::globalInstance()->acquireModel();
|
||||
acquireModel();
|
||||
#if defined(DEBUG_MODEL_LOADING)
|
||||
qDebug() << "acquired model from store" << m_llmThread.objectName() << m_llModelInfo.model.get();
|
||||
#endif
|
||||
@@ -251,8 +257,6 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
// reset status
|
||||
emit modelLoadingPercentageChanged(std::numeric_limits<float>::min()); // small non-zero positive value
|
||||
emit modelLoadingError("");
|
||||
emit reportFallbackReason("");
|
||||
emit reportDevice("");
|
||||
m_pristineLoadedState = false;
|
||||
|
||||
QString filePath = modelInfo.dirpath + modelInfo.filename();
|
||||
@@ -265,12 +269,12 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
#if defined(DEBUG_MODEL_LOADING)
|
||||
qDebug() << "already acquired model deleted" << m_llmThread.objectName() << m_llModelInfo.model.get();
|
||||
#endif
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
} else if (!m_isServer) {
|
||||
// This is a blocking call that tries to retrieve the model we need from the model store.
|
||||
// If it succeeds, then we just have to restore state. If the store has never had a model
|
||||
// returned to it, then the modelInfo.model pointer should be null which will happen on startup
|
||||
m_llModelInfo = LLModelStore::globalInstance()->acquireModel();
|
||||
acquireModel();
|
||||
#if defined(DEBUG_MODEL_LOADING)
|
||||
qDebug() << "acquired model from store" << m_llmThread.objectName() << m_llModelInfo.model.get();
|
||||
#endif
|
||||
@@ -305,7 +309,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
#if defined(DEBUG_MODEL_LOADING)
|
||||
qDebug() << "deleting model" << m_llmThread.objectName() << m_llModelInfo.model.get();
|
||||
#endif
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +339,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
model->setModelName(modelName);
|
||||
model->setRequestURL(modelInfo.url());
|
||||
model->setAPIKey(apiKey);
|
||||
m_llModelInfo.model.reset(model);
|
||||
m_llModelInfo.resetModel(this, model);
|
||||
} else {
|
||||
QElapsedTimer modelLoadTimer;
|
||||
modelLoadTimer.start();
|
||||
@@ -360,10 +364,10 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
#endif
|
||||
|
||||
QString constructError;
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
try {
|
||||
auto *model = LLModel::Implementation::construct(filePath.toStdString(), backend, n_ctx);
|
||||
m_llModelInfo.model.reset(model);
|
||||
m_llModelInfo.resetModel(this, model);
|
||||
} catch (const LLModel::MissingImplementationError &e) {
|
||||
modelLoadProps.insert("error", "missing_model_impl");
|
||||
constructError = e.what();
|
||||
@@ -412,14 +416,15 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
memGB = std::floor(memGB * 10.f) / 10.f; // truncate to 1 decimal place
|
||||
modelLoadProps.insert("default_device", QString::fromStdString(defaultDevice->name));
|
||||
modelLoadProps.insert("default_device_mem", approxDeviceMemGB(defaultDevice));
|
||||
modelLoadProps.insert("default_device_backend", QString::fromStdString(defaultDevice->backendName()));
|
||||
}
|
||||
}
|
||||
|
||||
QString actualDevice("CPU");
|
||||
bool actualDeviceIsCPU = true;
|
||||
|
||||
#if defined(Q_OS_MAC) && defined(__aarch64__)
|
||||
if (m_llModelInfo.model->implementation().buildVariant() == "metal")
|
||||
actualDevice = "Metal";
|
||||
actualDeviceIsCPU = false;
|
||||
#else
|
||||
if (requestedDevice != "CPU") {
|
||||
const auto *device = defaultDevice;
|
||||
@@ -437,41 +442,39 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
if (!device) {
|
||||
// GPU not available
|
||||
} else if (!m_llModelInfo.model->initializeGPUDevice(device->index, &unavail_reason)) {
|
||||
emit reportFallbackReason(QString::fromStdString("<br>" + unavail_reason));
|
||||
m_llModelInfo.fallbackReason = QString::fromStdString(unavail_reason);
|
||||
} else {
|
||||
actualDevice = QString::fromStdString(device->reportedName());
|
||||
actualDeviceIsCPU = false;
|
||||
modelLoadProps.insert("requested_device_mem", approxDeviceMemGB(device));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Report which device we're actually using
|
||||
emit reportDevice(actualDevice);
|
||||
bool success = m_llModelInfo.model->loadModel(filePath.toStdString(), n_ctx, ngl);
|
||||
|
||||
if (!m_shouldBeLoaded) {
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo));
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingPercentageChanged(0.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actualDevice == "CPU") {
|
||||
if (actualDeviceIsCPU) {
|
||||
// we asked llama.cpp to use the CPU
|
||||
} else if (!success) {
|
||||
// llama_init_from_file returned nullptr
|
||||
emit reportDevice("CPU");
|
||||
emit reportFallbackReason("<br>GPU loading failed (out of VRAM?)");
|
||||
m_llModelInfo.fallbackReason = "GPU loading failed (out of VRAM?)";
|
||||
modelLoadProps.insert("cpu_fallback_reason", "gpu_load_failed");
|
||||
success = m_llModelInfo.model->loadModel(filePath.toStdString(), n_ctx, 0);
|
||||
|
||||
if (!m_shouldBeLoaded) {
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo));
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingPercentageChanged(0.0f);
|
||||
return false;
|
||||
}
|
||||
@@ -479,16 +482,15 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
// ggml_vk_init was not called in llama.cpp
|
||||
// We might have had to fallback to CPU after load if the model is not possible to accelerate
|
||||
// for instance if the quantization method is not supported on Vulkan yet
|
||||
emit reportDevice("CPU");
|
||||
emit reportFallbackReason("<br>model or quant has no GPU support");
|
||||
m_llModelInfo.fallbackReason = "model or quant has no GPU support";
|
||||
modelLoadProps.insert("cpu_fallback_reason", "gpu_unsupported_model");
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo));
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingError(u"Could not load model due to invalid model file for %1"_s.arg(modelInfo.filename()));
|
||||
modelLoadProps.insert("error", "loadmodel_failed");
|
||||
} else {
|
||||
@@ -497,10 +499,10 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
case 'G': m_llModelType = LLModelType::GPTJ_; break;
|
||||
default:
|
||||
{
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo));
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingError(u"Could not determine model type for %1"_s.arg(modelInfo.filename()));
|
||||
}
|
||||
}
|
||||
@@ -510,7 +512,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
} else {
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo));
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingError(u"Error loading %1: %2"_s.arg(modelInfo.filename(), constructError));
|
||||
}
|
||||
}
|
||||
@@ -523,6 +525,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
fflush(stdout);
|
||||
#endif
|
||||
emit modelLoadingPercentageChanged(isModelLoaded() ? 1.0f : 0.0f);
|
||||
emit loadedModelInfoChanged();
|
||||
|
||||
modelLoadProps.insert("requestedDevice", MySettings::globalInstance()->device());
|
||||
modelLoadProps.insert("model", modelInfo.filename());
|
||||
@@ -530,7 +533,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo)
|
||||
} else {
|
||||
if (!m_isServer)
|
||||
LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo)); // release back into the store
|
||||
m_llModelInfo = LLModelInfo();
|
||||
resetModel();
|
||||
emit modelLoadingError(u"Could not find file for model %1"_s.arg(modelInfo.filename()));
|
||||
}
|
||||
|
||||
@@ -621,6 +624,16 @@ void ChatLLM::setModelInfo(const ModelInfo &modelInfo)
|
||||
emit modelInfoChanged(modelInfo);
|
||||
}
|
||||
|
||||
void ChatLLM::acquireModel() {
|
||||
m_llModelInfo = LLModelStore::globalInstance()->acquireModel();
|
||||
emit loadedModelInfoChanged();
|
||||
}
|
||||
|
||||
void ChatLLM::resetModel() {
|
||||
m_llModelInfo = {};
|
||||
emit loadedModelInfoChanged();
|
||||
}
|
||||
|
||||
void ChatLLM::modelChangeRequested(const ModelInfo &modelInfo)
|
||||
{
|
||||
m_shouldBeLoaded = true;
|
||||
@@ -809,7 +822,7 @@ void ChatLLM::unloadModel()
|
||||
#endif
|
||||
|
||||
if (m_forceUnloadModel) {
|
||||
m_llModelInfo.model.reset();
|
||||
m_llModelInfo.resetModel(this);
|
||||
m_forceUnloadModel = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
@@ -32,11 +33,17 @@ enum LLModelType {
|
||||
API_,
|
||||
};
|
||||
|
||||
class ChatLLM;
|
||||
|
||||
struct LLModelInfo {
|
||||
std::unique_ptr<LLModel> model;
|
||||
QFileInfo fileInfo;
|
||||
std::optional<QString> fallbackReason;
|
||||
|
||||
// NOTE: This does not store the model type or name on purpose as this is left for ChatLLM which
|
||||
// must be able to serialize the information even if it is in the unloaded state
|
||||
|
||||
void resetModel(ChatLLM *cllm, LLModel *model = nullptr);
|
||||
};
|
||||
|
||||
class TokenTimer : public QObject {
|
||||
@@ -84,6 +91,9 @@ class ChatLLM : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isRecalc READ isRecalc NOTIFY recalcChanged)
|
||||
Q_PROPERTY(QString deviceBackend READ deviceBackend NOTIFY loadedModelInfoChanged)
|
||||
Q_PROPERTY(QString device READ device NOTIFY loadedModelInfoChanged)
|
||||
Q_PROPERTY(QString fallbackReason READ fallbackReason NOTIFY loadedModelInfoChanged)
|
||||
public:
|
||||
ChatLLM(Chat *parent, bool isServer = false);
|
||||
virtual ~ChatLLM();
|
||||
@@ -110,6 +120,30 @@ public:
|
||||
|
||||
bool isRecalc() const { return m_isRecalc; }
|
||||
|
||||
void acquireModel();
|
||||
void resetModel();
|
||||
|
||||
QString deviceBackend() const
|
||||
{
|
||||
if (!isModelLoaded()) return QString();
|
||||
std::string name = LLModel::GPUDevice::backendIdToName(m_llModelInfo.model->backendName());
|
||||
return QString::fromStdString(name);
|
||||
}
|
||||
|
||||
QString device() const
|
||||
{
|
||||
if (!isModelLoaded()) return QString();
|
||||
const char *name = m_llModelInfo.model->gpuDeviceName();
|
||||
return name ? QString(name) : u"CPU"_s;
|
||||
}
|
||||
|
||||
// not loaded -> QString(), no fallback -> QString("")
|
||||
QString fallbackReason() const
|
||||
{
|
||||
if (!isModelLoaded()) return QString();
|
||||
return m_llModelInfo.fallbackReason.value_or(u""_s);
|
||||
}
|
||||
|
||||
QString generatedName() const { return QString::fromStdString(m_nameResponse); }
|
||||
|
||||
bool serialize(QDataStream &stream, int version, bool serializeKV);
|
||||
@@ -135,6 +169,7 @@ public Q_SLOTS:
|
||||
|
||||
Q_SIGNALS:
|
||||
void recalcChanged();
|
||||
void loadedModelInfoChanged();
|
||||
void modelLoadingPercentageChanged(float);
|
||||
void modelLoadingError(const QString &error);
|
||||
void modelLoadingWarning(const QString &warning);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -49,6 +50,20 @@ struct DocumentInfo
|
||||
};
|
||||
|
||||
struct ResultInfo {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString collection MEMBER collection)
|
||||
Q_PROPERTY(QString path MEMBER path)
|
||||
Q_PROPERTY(QString file MEMBER file)
|
||||
Q_PROPERTY(QString title MEMBER title)
|
||||
Q_PROPERTY(QString author MEMBER author)
|
||||
Q_PROPERTY(QString date MEMBER date)
|
||||
Q_PROPERTY(QString text MEMBER text)
|
||||
Q_PROPERTY(int page MEMBER page)
|
||||
Q_PROPERTY(int from MEMBER from)
|
||||
Q_PROPERTY(int to MEMBER to)
|
||||
Q_PROPERTY(QString fileUri READ fileUri STORED false)
|
||||
|
||||
public:
|
||||
QString collection; // [Required] The name of the collection
|
||||
QString path; // [Required] The full path
|
||||
QString file; // [Required] The name of the file, but not the full path
|
||||
@@ -60,6 +75,21 @@ struct ResultInfo {
|
||||
int from = -1; // [Optional] The line number where the text begins
|
||||
int to = -1; // [Optional] The line number where the text ends
|
||||
|
||||
QString fileUri() const {
|
||||
// QUrl reserved chars that are not UNSAFE_PATH according to glib/gconvert.c
|
||||
static const QByteArray s_exclude = "!$&'()*+,/:=@~"_ba;
|
||||
|
||||
Q_ASSERT(!QFileInfo(path).isRelative());
|
||||
#ifdef Q_OS_WINDOWS
|
||||
Q_ASSERT(!path.contains('\\')); // Qt normally uses forward slash as path separator
|
||||
#endif
|
||||
|
||||
auto escaped = QString::fromUtf8(QUrl::toPercentEncoding(path, s_exclude));
|
||||
if (escaped.front() != '/')
|
||||
escaped = '/' + escaped;
|
||||
return u"file://"_s + escaped;
|
||||
}
|
||||
|
||||
bool operator==(const ResultInfo &other) const {
|
||||
return file == other.file &&
|
||||
title == other.title &&
|
||||
@@ -73,18 +103,6 @@ struct ResultInfo {
|
||||
bool operator!=(const ResultInfo &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString collection MEMBER collection)
|
||||
Q_PROPERTY(QString path MEMBER path)
|
||||
Q_PROPERTY(QString file MEMBER file)
|
||||
Q_PROPERTY(QString title MEMBER title)
|
||||
Q_PROPERTY(QString author MEMBER author)
|
||||
Q_PROPERTY(QString date MEMBER date)
|
||||
Q_PROPERTY(QString text MEMBER text)
|
||||
Q_PROPERTY(int page MEMBER page)
|
||||
Q_PROPERTY(int from MEMBER from)
|
||||
Q_PROPERTY(int to MEMBER to)
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ResultInfo)
|
||||
|
||||
1
gpt4all-chat/icons/caret_down.svg
Normal file
1
gpt4all-chat/icons/caret_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 238 B |
1
gpt4all-chat/icons/caret_right.svg
Normal file
1
gpt4all-chat/icons/caret_right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -63,6 +63,7 @@ Window {
|
||||
|
||||
property bool hasCheckedFirstStart: false
|
||||
property bool hasShownSettingsAccess: false
|
||||
property var currentChat: ChatListModel.currentChat
|
||||
|
||||
function startupDialogs() {
|
||||
if (!LLM.compatHardware()) {
|
||||
@@ -396,6 +397,98 @@ Window {
|
||||
Layout.margins: 0
|
||||
spacing: 22
|
||||
|
||||
Item {
|
||||
id: antennaItem
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: antennaImage.width
|
||||
Layout.preferredHeight: antennaImage.height
|
||||
Image {
|
||||
id: antennaImage
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
visible: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
id: antennaColored
|
||||
visible: ModelList.installedModels.count !== 0 && (currentChat.isServer || currentChat.modelInfo.isOnline || MySettings.networkIsActive)
|
||||
anchors.fill: antennaImage
|
||||
source: antennaImage
|
||||
color: theme.styledTextColor
|
||||
ToolTip.text: {
|
||||
if (MySettings.networkIsActive)
|
||||
return qsTr("The datalake is enabled")
|
||||
else if (currentChat.modelInfo.isOnline)
|
||||
return qsTr("Using a network model")
|
||||
else if (currentChat.modelInfo.isOnline)
|
||||
return qsTr("Server mode is enabled")
|
||||
return ""
|
||||
}
|
||||
ToolTip.visible: maAntenna.containsMouse
|
||||
ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
|
||||
MouseArea {
|
||||
id: maAntenna
|
||||
anchors.fill: antennaColored
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_1.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 1500
|
||||
from: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_1.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.preferredWidth: image.width
|
||||
|
||||
@@ -298,6 +298,7 @@ void Network::trackChatEvent(const QString &ev, QVariantMap props)
|
||||
const auto &curChat = ChatListModel::globalInstance()->currentChat();
|
||||
if (!props.contains("model"))
|
||||
props.insert("model", curChat->modelInfo().filename());
|
||||
props.insert("device_backend", curChat->deviceBackend());
|
||||
props.insert("actualDevice", curChat->device());
|
||||
props.insert("doc_collections_enabled", curChat->collectionList().count());
|
||||
props.insert("doc_collections_total", LocalDocs::globalInstance()->localDocsModel()->rowCount());
|
||||
|
||||
@@ -38,12 +38,12 @@ Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 30
|
||||
spacing: 50
|
||||
spacing: 30
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: 50
|
||||
spacing: 30
|
||||
|
||||
MyButton {
|
||||
id: backButton
|
||||
@@ -84,7 +84,6 @@ Rectangle {
|
||||
readOnly: ModelList.discoverInProgress
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 90
|
||||
font.pixelSize: theme.fontSizeLarger
|
||||
placeholderText: qsTr("Discover and download models by keyword search...")
|
||||
Accessible.role: Accessible.EditableText
|
||||
@@ -630,7 +629,7 @@ Rectangle {
|
||||
color: theme.mutedDarkTextColor
|
||||
}
|
||||
Text {
|
||||
text: ramrequired + qsTr(" GB")
|
||||
text: ramrequired >= 0 ? ramrequired + qsTr(" GB") : "?"
|
||||
color: theme.textColor
|
||||
font.pixelSize: theme.fontSizeSmaller
|
||||
font.bold: true
|
||||
@@ -652,7 +651,7 @@ Rectangle {
|
||||
color: theme.mutedDarkTextColor
|
||||
}
|
||||
Text {
|
||||
text: parameters
|
||||
text: parameters !== "" ? parameters : "?"
|
||||
color: theme.textColor
|
||||
font.pixelSize: theme.fontSizeSmaller
|
||||
font.bold: true
|
||||
|
||||
@@ -77,7 +77,7 @@ MySettingsTab {
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,7 @@ MySettingsTab {
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
@@ -420,7 +420,7 @@ MySettingsTab {
|
||||
Accessible.description: ToolTip.text
|
||||
}
|
||||
|
||||
MySettingsLabel {
|
||||
/*MySettingsLabel {
|
||||
id: gpuOverrideLabel
|
||||
text: qsTr("Force Metal (macOS+arm)")
|
||||
Layout.row: 13
|
||||
@@ -437,7 +437,7 @@ MySettingsTab {
|
||||
}
|
||||
ToolTip.text: qsTr("WARNING: On macOS with arm (M1+) this setting forces usage of the GPU. Can cause crashes if the model requires more RAM than the system supports. Because of crash possibility the setting will not persist across restarts of the application. This has no effect on non-macs or intel.")
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}*/
|
||||
|
||||
MySettingsLabel {
|
||||
id: updatesLabel
|
||||
@@ -463,7 +463,7 @@ MySettingsTab {
|
||||
Layout.column: 0
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 2
|
||||
width: 1
|
||||
color: theme.dividerColor
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 20
|
||||
font.pixelSize: theme.fontSizeLarger
|
||||
topPadding: 20
|
||||
bottomPadding: 20
|
||||
topPadding: 24
|
||||
bottomPadding: 24
|
||||
text: qsTr("\uFF0B New chat")
|
||||
Accessible.description: qsTr("Create a new chat")
|
||||
onClicked: {
|
||||
@@ -59,7 +59,7 @@ Rectangle {
|
||||
id: divider
|
||||
anchors.top: newChat.bottom
|
||||
anchors.margins: 20
|
||||
anchors.topMargin: 15
|
||||
anchors.topMargin: 14
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
@@ -109,8 +109,8 @@ Rectangle {
|
||||
topPadding: 15
|
||||
bottomPadding: 5
|
||||
text: parent.section
|
||||
color: theme.styledTextColor
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
color: theme.chatDrawerSectionHeader
|
||||
font.pixelSize: theme.fontSizeSmaller
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,11 +191,11 @@ Rectangle {
|
||||
anchors.verticalCenter: chatName.verticalCenter
|
||||
anchors.right: chatRectangle.right
|
||||
anchors.rightMargin: 10
|
||||
spacing: 10
|
||||
spacing: 5
|
||||
MyToolButton {
|
||||
id: editButton
|
||||
width: 30
|
||||
height: 30
|
||||
imageWidth: 24
|
||||
imageHeight: 24
|
||||
visible: isCurrent && !isServer
|
||||
opacity: trashQuestionDisplayed ? 0.5 : 1.0
|
||||
source: "qrc:/gpt4all/icons/edit.svg"
|
||||
@@ -208,8 +208,8 @@ Rectangle {
|
||||
}
|
||||
MyToolButton {
|
||||
id: trashButton
|
||||
width: 30
|
||||
height: 30
|
||||
imageWidth: 24
|
||||
imageHeight: 24
|
||||
visible: isCurrent && !isServer
|
||||
source: "qrc:/gpt4all/icons/trash.svg"
|
||||
onClicked: {
|
||||
|
||||
@@ -321,7 +321,7 @@ Rectangle {
|
||||
}
|
||||
delegate: ItemDelegate {
|
||||
id: comboItemDelegate
|
||||
width: comboBox.width
|
||||
width: comboItemPopup.width
|
||||
contentItem: Text {
|
||||
text: name
|
||||
color: theme.textColor
|
||||
@@ -336,6 +336,35 @@ Rectangle {
|
||||
}
|
||||
highlighted: comboBox.highlightedIndex === index
|
||||
}
|
||||
popup: Popup {
|
||||
id: comboItemPopup
|
||||
y: comboBox.height - 1
|
||||
width: comboBox.width
|
||||
implicitHeight: Math.min(window.height - y, contentItem.implicitHeight)
|
||||
padding: 0
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: comboBox.width
|
||||
implicitHeight: comboItemPopupListView.implicitHeight
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ListView {
|
||||
id: comboItemPopupListView
|
||||
implicitHeight: contentHeight
|
||||
model: comboBox.popup.visible ? comboBox.delegateModel : null
|
||||
currentIndex: comboBox.highlightedIndex
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: theme.black
|
||||
}
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.ComboBox
|
||||
Accessible.name: currentModelName()
|
||||
Accessible.description: qsTr("The top item is the current model")
|
||||
@@ -496,7 +525,7 @@ Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: theme.conversationDivider
|
||||
height: 2
|
||||
height: 1
|
||||
}
|
||||
|
||||
CollectionsDrawer {
|
||||
@@ -685,8 +714,8 @@ Rectangle {
|
||||
Accessible.description: qsTr("prompt / response pairs from the conversation")
|
||||
|
||||
delegate: GridLayout {
|
||||
width: listView.contentItem.width
|
||||
rows: 4
|
||||
width: listView.contentItem.width - 15
|
||||
rows: 3
|
||||
columns: 2
|
||||
|
||||
Item {
|
||||
@@ -768,53 +797,286 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item {
|
||||
ColumnLayout {
|
||||
Layout.row: 1
|
||||
Layout.column: 0
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.topMargin: 5
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences
|
||||
Layout.column: 1
|
||||
Layout.fillWidth: true
|
||||
TextArea {
|
||||
id: myTextArea
|
||||
text: value
|
||||
Layout.fillWidth: true
|
||||
padding: 0
|
||||
color: {
|
||||
if (!currentChat.isServer)
|
||||
return theme.textColor
|
||||
if (name === qsTr("Response: "))
|
||||
return theme.white
|
||||
return theme.black
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: TextEdit.PlainText
|
||||
focus: false
|
||||
readOnly: true
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
cursorVisible: currentResponse ? currentChat.responseInProgress : false
|
||||
cursorPosition: text.length
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
onTapped: function(eventPoint, button) {
|
||||
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
|
||||
var success = responseText.tryCopyAtPosition(clickedPos);
|
||||
if (success)
|
||||
copyCodeMessage.open();
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: sourcesIcon
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
mipmap: true
|
||||
source: "qrc:/gpt4all/icons/db.svg"
|
||||
MouseArea {
|
||||
id: conversationMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
conversationContextMenu.x = conversationMouseArea.mouseX
|
||||
conversationContextMenu.y = conversationMouseArea.mouseY
|
||||
conversationContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: conversationContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: myTextArea.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: myTextArea.copy()
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Copy Message")
|
||||
enabled: myTextArea.selectedText === ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: {
|
||||
myTextArea.selectAll()
|
||||
myTextArea.copy()
|
||||
myTextArea.deselect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResponseText {
|
||||
id: responseText
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
responseText.setLinkColor(theme.linkColor);
|
||||
responseText.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast);
|
||||
responseText.textDocument = textDocument
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.Paragraph
|
||||
Accessible.name: text
|
||||
Accessible.description: name === qsTr("Response: ") ? "The response by the model" : "The prompt by the user"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: sourcesIcon
|
||||
source: sourcesIcon
|
||||
color: theme.textColor
|
||||
ThumbsDownDialog {
|
||||
id: thumbsDownDialog
|
||||
property point globalPoint: mapFromItem(window,
|
||||
window.width / 2 - width / 2,
|
||||
window.height / 2 - height / 2)
|
||||
x: globalPoint.x
|
||||
y: globalPoint.y
|
||||
width: 640
|
||||
height: 300
|
||||
property string text: value
|
||||
response: newResponse === undefined || newResponse === "" ? text : newResponse
|
||||
onAccepted: {
|
||||
var responseHasChanged = response !== text && response !== newResponse
|
||||
if (thumbsDownState && !thumbsUpState && !responseHasChanged)
|
||||
return
|
||||
|
||||
chatModel.updateNewResponse(index, response)
|
||||
chatModel.updateThumbsUpState(index, false)
|
||||
chatModel.updateThumbsDownState(index, true)
|
||||
Network.sendConversation(currentChat.id, getConversationJson());
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: 15
|
||||
visible: name === qsTr("Response: ") &&
|
||||
(!currentResponse || !currentChat.responseInProgress) && MySettings.networkIsActive
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
MyToolButton {
|
||||
id: thumbsUp
|
||||
width: 24
|
||||
height: 24
|
||||
imageWidth: width
|
||||
imageHeight: height
|
||||
opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
|
||||
source: "qrc:/gpt4all/icons/thumbs_up.svg"
|
||||
Accessible.name: qsTr("Thumbs up")
|
||||
Accessible.description: qsTr("Gives a thumbs up to the response")
|
||||
onClicked: {
|
||||
if (thumbsUpState && !thumbsDownState)
|
||||
return
|
||||
|
||||
chatModel.updateNewResponse(index, "")
|
||||
chatModel.updateThumbsUpState(index, true)
|
||||
chatModel.updateThumbsDownState(index, false)
|
||||
Network.sendConversation(currentChat.id, getConversationJson());
|
||||
}
|
||||
}
|
||||
|
||||
MyToolButton {
|
||||
id: thumbsDown
|
||||
anchors.top: thumbsUp.top
|
||||
anchors.topMargin: 10
|
||||
anchors.left: thumbsUp.right
|
||||
anchors.leftMargin: 2
|
||||
width: 24
|
||||
height: 24
|
||||
imageWidth: width
|
||||
imageHeight: height
|
||||
checked: thumbsDownState
|
||||
opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
|
||||
transform: [
|
||||
Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
},
|
||||
Translate {
|
||||
x: thumbsDown.width
|
||||
}
|
||||
]
|
||||
source: "qrc:/gpt4all/icons/thumbs_down.svg"
|
||||
Accessible.name: qsTr("Thumbs down")
|
||||
Accessible.description: qsTr("Opens thumbs down dialog")
|
||||
onClicked: {
|
||||
thumbsDownDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.row: 1
|
||||
Item {
|
||||
Layout.row: 2
|
||||
Layout.column: 1
|
||||
Layout.topMargin: 5
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences
|
||||
TextArea {
|
||||
text: qsTr("Sources")
|
||||
padding: 0
|
||||
readOnly: true
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
font.bold: true
|
||||
color: theme.textColor
|
||||
Layout.preferredWidth: childrenRect.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences && (!currentResponse || !currentChat.responseInProgress)
|
||||
|
||||
MyButton {
|
||||
backgroundColor: theme.sourcesBackground
|
||||
backgroundColorHovered: theme.sourcesBackgroundHovered
|
||||
contentItem: RowLayout {
|
||||
anchors.centerIn: parent
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
|
||||
Image {
|
||||
id: sourcesIcon
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
mipmap: true
|
||||
source: "qrc:/gpt4all/icons/db.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: sourcesIcon
|
||||
source: sourcesIcon
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
TextArea {
|
||||
text: consolidatedSources.length + " " + qsTr("Sources")
|
||||
padding: 0
|
||||
readOnly: true
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
font.bold: true
|
||||
color: theme.styledTextColor
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: caret.width
|
||||
Layout.preferredHeight: caret.height
|
||||
Image {
|
||||
id: caret
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
sourceSize.width: theme.fontSizeLarge
|
||||
sourceSize.height: theme.fontSizeLarge
|
||||
mipmap: true
|
||||
source: {
|
||||
if (sourcesLayout.state === "collapsed")
|
||||
return "qrc:/gpt4all/icons/caret_right.svg";
|
||||
else
|
||||
return "qrc:/gpt4all/icons/caret_down.svg";
|
||||
}
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: caret
|
||||
source: caret
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (sourcesLayout.state === "collapsed")
|
||||
sourcesLayout.state = "expanded";
|
||||
else
|
||||
sourcesLayout.state = "collapsed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.row: 2
|
||||
id: sourcesLayout
|
||||
Layout.row: 3
|
||||
Layout.column: 1
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences && (!currentResponse || !currentChat.responseInProgress)
|
||||
clip: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 0
|
||||
state: "collapsed"
|
||||
states: [
|
||||
State {
|
||||
name: "expanded"
|
||||
PropertyChanges { target: sourcesLayout; Layout.preferredHeight: childrenRect.height }
|
||||
},
|
||||
State {
|
||||
name: "collapsed"
|
||||
PropertyChanges { target: sourcesLayout; Layout.preferredHeight: 0 }
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
SequentialAnimation {
|
||||
PropertyAnimation {
|
||||
target: sourcesLayout
|
||||
property: "Layout.preferredHeight"
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 5
|
||||
@@ -835,7 +1097,7 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: function() {
|
||||
Qt.openUrlExternally("file://" + modelData.path)
|
||||
Qt.openUrlExternally(modelData.fileUri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,212 +1187,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.row: 3
|
||||
Layout.column: 0
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
Layout.topMargin: 5
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences
|
||||
Image {
|
||||
id: answersIcon
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
mipmap: true
|
||||
source: "qrc:/gpt4all/icons/info.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
anchors.fill: answersIcon
|
||||
source: answersIcon
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.row: 3
|
||||
Layout.column: 1
|
||||
Layout.topMargin: 5
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: consolidatedSources.length !== 0 && MySettings.localDocsShowReferences
|
||||
TextArea {
|
||||
text: qsTr("Answer")
|
||||
padding: 0
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
font.bold: true
|
||||
readOnly: true
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.row: 4
|
||||
Layout.column: 1
|
||||
Layout.fillWidth: true
|
||||
TextArea {
|
||||
id: myTextArea
|
||||
text: value
|
||||
Layout.fillWidth: true
|
||||
padding: 0
|
||||
color: {
|
||||
if (!currentChat.isServer)
|
||||
return theme.textColor
|
||||
if (name === qsTr("Response: "))
|
||||
return theme.white
|
||||
return theme.black
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: TextEdit.PlainText
|
||||
focus: false
|
||||
readOnly: true
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
cursorVisible: currentResponse ? currentChat.responseInProgress : false
|
||||
cursorPosition: text.length
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
onTapped: function(eventPoint, button) {
|
||||
var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y);
|
||||
var success = responseText.tryCopyAtPosition(clickedPos);
|
||||
if (success)
|
||||
copyCodeMessage.open();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: conversationMouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
conversationContextMenu.x = conversationMouseArea.mouseX
|
||||
conversationContextMenu.y = conversationMouseArea.mouseY
|
||||
conversationContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: conversationContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("Copy")
|
||||
enabled: myTextArea.selectedText !== ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: myTextArea.copy()
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Copy Message")
|
||||
enabled: myTextArea.selectedText === ""
|
||||
height: enabled ? implicitHeight : 0
|
||||
onTriggered: {
|
||||
myTextArea.selectAll()
|
||||
myTextArea.copy()
|
||||
myTextArea.deselect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResponseText {
|
||||
id: responseText
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
responseText.setLinkColor(theme.linkColor);
|
||||
responseText.setHeaderColor(name === qsTr("Response: ") ? theme.darkContrast : theme.lightContrast);
|
||||
responseText.textDocument = textDocument
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.Paragraph
|
||||
Accessible.name: text
|
||||
Accessible.description: name === qsTr("Response: ") ? "The response by the model" : "The prompt by the user"
|
||||
}
|
||||
|
||||
ThumbsDownDialog {
|
||||
id: thumbsDownDialog
|
||||
property point globalPoint: mapFromItem(window,
|
||||
window.width / 2 - width / 2,
|
||||
window.height / 2 - height / 2)
|
||||
x: globalPoint.x
|
||||
y: globalPoint.y
|
||||
property string text: value
|
||||
response: newResponse === undefined || newResponse === "" ? text : newResponse
|
||||
onAccepted: {
|
||||
var responseHasChanged = response !== text && response !== newResponse
|
||||
if (thumbsDownState && !thumbsUpState && !responseHasChanged)
|
||||
return
|
||||
|
||||
chatModel.updateNewResponse(index, response)
|
||||
chatModel.updateThumbsUpState(index, false)
|
||||
chatModel.updateThumbsDownState(index, true)
|
||||
Network.sendConversation(currentChat.id, getConversationJson());
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.rightMargin: 15
|
||||
visible: name === qsTr("Response: ") &&
|
||||
(!currentResponse || !currentChat.responseInProgress) && MySettings.networkIsActive
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
MyToolButton {
|
||||
id: thumbsUp
|
||||
width: 24
|
||||
height: 24
|
||||
imageWidth: width
|
||||
imageHeight: height
|
||||
opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
|
||||
source: "qrc:/gpt4all/icons/thumbs_up.svg"
|
||||
Accessible.name: qsTr("Thumbs up")
|
||||
Accessible.description: qsTr("Gives a thumbs up to the response")
|
||||
onClicked: {
|
||||
if (thumbsUpState && !thumbsDownState)
|
||||
return
|
||||
|
||||
chatModel.updateNewResponse(index, "")
|
||||
chatModel.updateThumbsUpState(index, true)
|
||||
chatModel.updateThumbsDownState(index, false)
|
||||
Network.sendConversation(currentChat.id, getConversationJson());
|
||||
}
|
||||
}
|
||||
|
||||
MyToolButton {
|
||||
id: thumbsDown
|
||||
anchors.top: thumbsUp.top
|
||||
anchors.topMargin: 10
|
||||
anchors.left: thumbsUp.right
|
||||
anchors.leftMargin: 2
|
||||
width: 24
|
||||
height: 24
|
||||
imageWidth: width
|
||||
imageHeight: height
|
||||
checked: thumbsDownState
|
||||
opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2
|
||||
transform: [
|
||||
Matrix4x4 {
|
||||
matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
||||
},
|
||||
Translate {
|
||||
x: thumbsDown.width
|
||||
}
|
||||
]
|
||||
source: "qrc:/gpt4all/icons/thumbs_down.svg"
|
||||
Accessible.name: qsTr("Thumbs down")
|
||||
Accessible.description: qsTr("Opens thumbs down dialog")
|
||||
onClicked: {
|
||||
thumbsDownDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool shouldAutoScroll: true
|
||||
@@ -1139,11 +1195,15 @@ Rectangle {
|
||||
Connections {
|
||||
target: currentChat
|
||||
function onResponseChanged() {
|
||||
if (listView.shouldAutoScroll) {
|
||||
listView.isAutoScrolling = true
|
||||
listView.positionViewAtEnd()
|
||||
listView.isAutoScrolling = false
|
||||
}
|
||||
listView.scrollToEnd()
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToEnd() {
|
||||
if (listView.shouldAutoScroll) {
|
||||
listView.isAutoScrolling = true
|
||||
listView.positionViewAtEnd()
|
||||
listView.isAutoScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,7 +1325,21 @@ Rectangle {
|
||||
visible: currentChat.tokenSpeed !== ""
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.WordWrap
|
||||
text: currentChat.tokenSpeed + " \u00B7 " + currentChat.device + currentChat.fallbackReason
|
||||
text: {
|
||||
const segments = [currentChat.tokenSpeed];
|
||||
const device = currentChat.device;
|
||||
const backend = currentChat.deviceBackend;
|
||||
if (device !== null) { // device is null if we have no model loaded
|
||||
var deviceSegment = device;
|
||||
if (backend === "CUDA" || backend === "Vulkan")
|
||||
deviceSegment += ` (${backend})`;
|
||||
segments.push(deviceSegment);
|
||||
}
|
||||
const fallbackReason = currentChat.fallbackReason;
|
||||
if (fallbackReason !== null && fallbackReason !== "")
|
||||
segments.push(fallbackReason);
|
||||
return segments.join(" \u00B7 ");
|
||||
}
|
||||
font.pixelSize: theme.fontSizeSmaller
|
||||
font.bold: true
|
||||
}
|
||||
@@ -1375,78 +1449,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: antennaImage
|
||||
anchors.right: sendButton.left
|
||||
anchors.verticalCenter: textInputView.verticalCenter
|
||||
anchors.rightMargin: 15
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
visible: false
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
visible: ModelList.installedModels.count !== 0 && (currentChat.isServer || currentChat.modelInfo.isOnline || MySettings.networkIsActive)
|
||||
anchors.fill: antennaImage
|
||||
source: antennaImage
|
||||
color: theme.styledTextColor
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_1.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 500
|
||||
from: "qrc:/gpt4all/icons/antenna_3.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 1500
|
||||
}
|
||||
|
||||
PropertyAnimation {
|
||||
target: antennaImage
|
||||
property: "source"
|
||||
duration: 1500
|
||||
from: "qrc:/gpt4all/icons/antenna_2.svg"
|
||||
to: "qrc:/gpt4all/icons/antenna_1.svg"
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 500
|
||||
}
|
||||
}
|
||||
|
||||
MyToolButton {
|
||||
id: sendButton
|
||||
backgroundColor: theme.sendButtonBackground
|
||||
|
||||
@@ -21,7 +21,7 @@ Rectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
width: 2
|
||||
width: 1
|
||||
color: theme.dividerColor
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ MySettingsTab {
|
||||
Rectangle {
|
||||
Layout.bottomMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ MySettingsTab {
|
||||
Rectangle {
|
||||
Layout.bottomMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.grayRed500
|
||||
}
|
||||
|
||||
@@ -117,24 +117,29 @@ MySettingsTab {
|
||||
MySettingsLabel {
|
||||
id: apiKeyLabel
|
||||
text: qsTr("Nomic API Key")
|
||||
helpText: qsTr("API key to use for Nomic Embed. Get one at https://atlas.nomic.ai/")
|
||||
helpText: qsTr('API key to use for Nomic Embed. Get one from the Atlas <a href="https://atlas.nomic.ai/cli-login">API keys page</a>.')
|
||||
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
|
||||
}
|
||||
|
||||
MyTextField {
|
||||
id: apiKeyField
|
||||
|
||||
property bool isValid: validate()
|
||||
onTextChanged: { isValid = validate(); }
|
||||
function validate() { return /^(nk-[a-zA-Z0-9_-]{43})?$/.test(apiKeyField.text); }
|
||||
|
||||
placeholderText: "nk-" + "X".repeat(43)
|
||||
text: MySettings.localDocsNomicAPIKey
|
||||
color: apiKeyField.acceptableInput ? theme.textColor : theme.textErrorColor
|
||||
color: apiKeyField.isValid ? theme.textColor : theme.textErrorColor
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.minimumWidth: 200
|
||||
enabled: useNomicAPIBox.checked
|
||||
validator: RegularExpressionValidator {
|
||||
// may be empty
|
||||
regularExpression: /|nk-[a-zA-Z0-9_-]{43}/
|
||||
}
|
||||
onEditingFinished: {
|
||||
MySettings.localDocsNomicAPIKey = apiKeyField.text;
|
||||
MySettings.localDocsUseRemoteEmbed = useNomicAPIBox.checked && MySettings.localDocsNomicAPIKey !== "";
|
||||
if (apiKeyField.isValid) {
|
||||
MySettings.localDocsNomicAPIKey = apiKeyField.text;
|
||||
MySettings.localDocsUseRemoteEmbed = useNomicAPIBox.checked && MySettings.localDocsNomicAPIKey !== "";
|
||||
}
|
||||
focus = false;
|
||||
}
|
||||
Accessible.role: Accessible.EditableText
|
||||
@@ -154,7 +159,7 @@ MySettingsTab {
|
||||
Rectangle {
|
||||
Layout.bottomMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.grayRed500
|
||||
}
|
||||
|
||||
@@ -184,7 +189,7 @@ MySettingsTab {
|
||||
Rectangle {
|
||||
Layout.bottomMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
|
||||
@@ -254,7 +259,7 @@ MySettingsTab {
|
||||
Rectangle {
|
||||
Layout.topMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ MySettingsTab {
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
@@ -803,7 +803,7 @@ MySettingsTab {
|
||||
Layout.columnSpan: 2
|
||||
Layout.topMargin: 15
|
||||
Layout.fillWidth: true
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ ComboBox {
|
||||
highlighted: comboBox.highlightedIndex === index
|
||||
}
|
||||
popup: Popup {
|
||||
// FIXME This should be made much nicer to take into account lists that are very long so
|
||||
// that it is scrollable and also sized optimally taking into account the x,y and the content
|
||||
// width and height as well as the window width and height
|
||||
y: comboBox.height - 1
|
||||
width: comboBox.width
|
||||
implicitHeight: contentItem.implicitHeight
|
||||
|
||||
@@ -33,8 +33,10 @@ Dialog {
|
||||
y: 0 - myDialog.padding + 15
|
||||
z: 300
|
||||
visible: myDialog.closePolicy != Popup.NoAutoClose
|
||||
width: 30
|
||||
height: 30
|
||||
width: 24
|
||||
height: 24
|
||||
imageWidth: 24
|
||||
imageHeight: 24
|
||||
padding: 0
|
||||
source: "qrc:/gpt4all/icons/close.svg"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
@@ -37,5 +37,11 @@ ColumnLayout {
|
||||
onLinkActivated: function(link) {
|
||||
root.linkActivated(link);
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // pass clicks to parent
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ Item {
|
||||
anchors.rightMargin: 15
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 2
|
||||
height: 1
|
||||
color: theme.settingsDivider
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ QtObject {
|
||||
property color green950: Qt.hsla(125/360, 0.22, 0.10)
|
||||
|
||||
// yellow
|
||||
property color yellow0: Qt.hsla(47/360, 0.90, 0.99)
|
||||
property color yellow25: Qt.hsla(47/360, 0.90, 0.98)
|
||||
property color yellow50: Qt.hsla(47/360, 0.90, 0.96)
|
||||
property color yellow100: Qt.hsla(46/360, 0.89, 0.89)
|
||||
@@ -194,14 +195,7 @@ QtObject {
|
||||
}
|
||||
|
||||
property color conversationDivider: {
|
||||
switch (MySettings.chatTheme) {
|
||||
case "LegacyDark":
|
||||
return blue900;
|
||||
case "Dark":
|
||||
return darkgray100;
|
||||
default:
|
||||
return gray100;
|
||||
}
|
||||
return dividerColor;
|
||||
}
|
||||
|
||||
property color settingsDivider: {
|
||||
@@ -980,6 +974,17 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
property color chatDrawerSectionHeader: {
|
||||
switch (MySettings.chatTheme) {
|
||||
case "LegacyDark":
|
||||
return purple50
|
||||
case "Dark":
|
||||
return yellow0
|
||||
default:
|
||||
return grayRed800;
|
||||
}
|
||||
}
|
||||
|
||||
property color dialogBorder: {
|
||||
switch (MySettings.chatTheme) {
|
||||
case "LegacyDark":
|
||||
|
||||
@@ -44,9 +44,9 @@ MyDialog {
|
||||
|
||||
ScrollView {
|
||||
clip: true
|
||||
height: 300
|
||||
height: 120
|
||||
width: parent.width
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
MyTextArea {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <QTextCharFormat>
|
||||
#include <QTextCursor>
|
||||
#include <QTextDocument>
|
||||
#include <QTextDocumentFragment>
|
||||
#include <QTextFrame>
|
||||
#include <QTextFrameFormat>
|
||||
#include <QTextTableCell>
|
||||
@@ -925,6 +926,17 @@ void ResponseText::handleTextChanged()
|
||||
m_isProcessingText = false;
|
||||
}
|
||||
|
||||
void replaceAndInsertMarkdown(int startIndex, int endIndex, QTextDocument *doc)
|
||||
{
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(startIndex);
|
||||
cursor.setPosition(endIndex, QTextCursor::KeepAnchor);
|
||||
QTextDocumentFragment fragment(cursor);
|
||||
const QString plainText = fragment.toPlainText();
|
||||
cursor.removeSelectedText();
|
||||
cursor.insertMarkdown(plainText);
|
||||
}
|
||||
|
||||
void ResponseText::handleCodeBlocks()
|
||||
{
|
||||
QTextDocument* doc = m_textDocument->textDocument();
|
||||
@@ -986,7 +998,16 @@ void ResponseText::handleCodeBlocks()
|
||||
|
||||
QVector<CodeCopy> newCopies;
|
||||
|
||||
// Track the position in the document to handle non-code blocks
|
||||
int lastIndex = 0;
|
||||
|
||||
for(int index = matchesCode.count() - 1; index >= 0; --index) {
|
||||
|
||||
int nonCodeStart = lastIndex;
|
||||
int nonCodeEnd = matchesCode[index].capturedStart();
|
||||
if (nonCodeEnd > nonCodeStart)
|
||||
replaceAndInsertMarkdown(nonCodeStart, nonCodeEnd, doc);
|
||||
|
||||
cursor.setPosition(matchesCode[index].capturedStart());
|
||||
cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor);
|
||||
cursor.removeSelectedText();
|
||||
@@ -1079,7 +1100,12 @@ void ResponseText::handleCodeBlocks()
|
||||
|
||||
cursor = mainFrame->lastCursorPosition();
|
||||
cursor.setCharFormat(QTextCharFormat());
|
||||
|
||||
lastIndex = matchesCode[index].capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < doc->characterCount())
|
||||
replaceAndInsertMarkdown(lastIndex, doc->characterCount() - 1, doc);
|
||||
|
||||
m_copies = newCopies;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user