Compare commits

...

22 Commits

Author SHA1 Message Date
Adam Treat
d92252cab1 Revert an incorrect renaming that slipped in.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 11:14:12 -04:00
Jared Van Bortel
6506ba161b UI tweaks for GPT4All v3.0.0-rc2 (#2474)
* clickable link to get API key with hand-style mouse cursor
* remove "Force Metal" setting
* allow typing incorrect API keys (but don't accept them), add placeholder text

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-06-27 11:08:32 -04:00
Adam Treat
bed92046d0 Set the 3.0.0-rc2 version.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 11:00:00 -04:00
Adam Treat
a1ec6f2150 Change the divider height and color to be more consistent.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 07:25:39 -04:00
Adam Treat
8d6e11fcad Change to just sources after multiple feedback advising same.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 07:25:39 -04:00
Adam Treat
fc5dc9dd1a Fix the scrollbar so it doesn't overlap content on chat view.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 07:25:39 -04:00
Adam Treat
d4494602e2 Markdown support.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-27 07:25:39 -04:00
AT
23e8f43c5a Change the way we're showing the localdocs sources. (#2475)
* Change the way we're showing the localdocs sources.

Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 22:00:48 -04:00
Adam Treat
31fa575c35 Place the antenna icon in the lower left right above nomic logo as per discussion.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 16:43:22 -04:00
Adam Treat
6d593d6ea1 Fix the thumbsdown dialog.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 16:37:01 -04:00
Jared Van Bortel
01870b4a46 chat: fix blank device in UI and improve Mixpanel reporting (#2409)
Also remove LLModel::hasGPUDevice.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-06-26 15:26:27 -04:00
Adam Treat
53fc2d56f6 Add a tooltip to make clear what is going on with the antenna animation.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 15:20:09 -04:00
Adam Treat
e5d9936d04 Update the license.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 15:07:51 -04:00
Adam Treat
11823022e2 Add a fixme for combobox popups in general which is less than ideal right now.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 15:00:50 -04:00
Jared Van Bortel
da1823ed7a cmake: fix CMAKE_CUDA_ARCHITECTURES default (#2421)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-06-26 14:48:18 -04:00
AT
3a61070f82 chat: fix incorrect file URIs for sources on Windows (#2469)
This was causing LocalDocs sources to not open correctly on Windows.

Signed-off-by: Adam Treat <treat.adam@gmail.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
2024-06-26 14:48:02 -04:00
Adam Treat
c87ccf4124 Make the chatview combo scrollable.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 14:39:39 -04:00
Adam Treat
88f5face2b Change section headers to be lighter and smaller as per Vincent.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 13:48:02 -04:00
Adam Treat
f8a935d8a6 Decrease vertical size of search bar and spacing in add model view.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 13:00:01 -04:00
Adam Treat
029bd318e9 If huggingface search doesn't give this information, then display question mark.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 12:31:11 -04:00
Adam Treat
d5968f4ab2 Make the chatdrawer edit/delete icons smaller.
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-26 12:19:02 -04:00
John W. Parent
30febbe3d2 Add basic Macos signing + notarizing workflow (#2319)
Adds basic CircleCI workflow to sign, notarize,
and staple MacOS app bundle and associated DMG,
then publishes signed binary in CircleCI artifacts

Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-06-25 20:31:51 -04:00
36 changed files with 802 additions and 491 deletions

View File

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

View 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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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".
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ Rectangle {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 2
width: 1
color: theme.dividerColor
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ Item {
anchors.rightMargin: 15
anchors.left: parent.left
anchors.right: parent.right
height: 2
height: 1
color: theme.settingsDivider
}

View File

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

View File

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

View File

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