Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5969d530bd | |||
| 809f1c6614 | |||
| 851e681cf5 | |||
| f2f3b7cce0 | |||
| 5b7a9b681c | |||
| 29af277139 | |||
| a45786bd00 | |||
| 695b35b510 | |||
| 5a23ab9c5a | |||
| c36dffea93 | |||
| 4b7eed2779 | |||
| 88c11c4702 | |||
| 543c79161d | |||
| aa2edf5954 | |||
| 894fec860a | |||
| e4324f8e80 | |||
| 6a0198ae9b | |||
| e136d6056a | |||
| ff027b12af | |||
| 0bdf77f38d | |||
| 21814e8809 | |||
| d732e2f9aa | |||
| bf6d09a068 | |||
| c3f2011c29 | |||
| af3fdb58ff | |||
| 637a4d9d4c | |||
| 7e2345773f | |||
| 14a5ddbdd8 | |||
| e178b7daa7 | |||
| 4b353d5091 | |||
| f7ba7b95be | |||
| 6ae95fec45 | |||
| dad8ab2bf3 | |||
| 25a6983de0 | |||
| 4e05abc7d2 | |||
| 784529e344 | |||
| 155153a763 | |||
| 9225c0c1a9 | |||
| 43adc95857 | |||
| ee672f2cda | |||
| a3edb8a577 | |||
| 407d3b11c0 | |||
| 285e739074 | |||
| f7e748ba7e | |||
| acb1306321 | |||
| 8b38ecc29b | |||
| cfb364f033 | |||
| 2fe6850a06 | |||
| 3e9506ca92 |
24
.github/scripts/plugin.json
vendored
@ -13,7 +13,7 @@
|
||||
"Linux"
|
||||
],
|
||||
"license": "GPLv3",
|
||||
"version": "0.5.8",
|
||||
"version": "0.5.11",
|
||||
"status": "draft",
|
||||
"is_pack": false,
|
||||
"released_at": null,
|
||||
@ -55,8 +55,28 @@
|
||||
},
|
||||
{
|
||||
"version": "0.5.8",
|
||||
"is_latest": true,
|
||||
"is_latest": false,
|
||||
"released_at": "2025-04-17T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"version": "0.5.9",
|
||||
"is_latest": false,
|
||||
"released_at": "2025-04-21T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"version": "0.5.10",
|
||||
"is_latest": false,
|
||||
"released_at": "2025-04-24T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"version": "0.5.11",
|
||||
"is_latest": false,
|
||||
"released_at": "2025-04-24T21:00:00Z"
|
||||
},
|
||||
{
|
||||
"version": "0.5.12",
|
||||
"is_latest": true,
|
||||
"released_at": "2025-05-01T17:00:00Z"
|
||||
}
|
||||
],
|
||||
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
||||
|
||||
99
.github/workflows/build_cmake.yml
vendored
@ -12,16 +12,13 @@ on:
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: QodeAssist
|
||||
QT_VERSION: 6.8.3
|
||||
QT_CREATOR_VERSION: 16.0.1
|
||||
QT_CREATOR_VERSION_INTERNAL: 16.0.1
|
||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||
CMAKE_VERSION: "3.29.6"
|
||||
NINJA_VERSION: "1.12.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.config.name }}
|
||||
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
outputs:
|
||||
tag: ${{ steps.git.outputs.tag }}
|
||||
@ -47,12 +44,18 @@ jobs:
|
||||
platform: mac_x64,
|
||||
cc: "clang", cxx: "clang++"
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.9.2",
|
||||
qt_creator_version: "17.0.1"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.2"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
@ -61,7 +64,12 @@ jobs:
|
||||
if (${{github.ref}} MATCHES "tags/v(.*)")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||
else()
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
||||
execute_process(
|
||||
COMMAND git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE short_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
|
||||
endif()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
@ -96,7 +104,7 @@ jobs:
|
||||
id: qt
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(qt_version "$ENV{QT_VERSION}")
|
||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
@ -117,7 +125,11 @@ jobs:
|
||||
set(url_os "mac_x64")
|
||||
set(qt_package_arch_suffix "clang_64")
|
||||
set(qt_dir_prefix "${qt_version}/macos")
|
||||
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||
if (qt_version VERSION_LESS "6.9.1")
|
||||
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||
else()
|
||||
set(qt_package_suffix "-MacOS-MacOS_15-Clang-MacOS-MacOS_15-X86_64-ARM64")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
||||
@ -140,7 +152,7 @@ jobs:
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||
endfunction()
|
||||
|
||||
foreach(package qtbase qtdeclarative)
|
||||
foreach(package qtbase qtdeclarative qttools)
|
||||
downloadAndExtract(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
${package}.7z
|
||||
@ -174,10 +186,11 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Download Qt Creator
|
||||
uses: qt-creator/install-dev-package@v1.2
|
||||
uses: qt-creator/install-dev-package@v2.0
|
||||
with:
|
||||
version: ${{ env.QT_CREATOR_VERSION }}
|
||||
version: ${{ matrix.qt_config.qt_creator_version }}
|
||||
unzip-to: 'qtcreator'
|
||||
platform: ${{ matrix.config.platform }}
|
||||
|
||||
- name: Extract Qt Creator
|
||||
id: qt_creator
|
||||
@ -223,7 +236,7 @@ jobs:
|
||||
COMMAND python
|
||||
-u
|
||||
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
|
||||
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
||||
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}"
|
||||
--src .
|
||||
--build build
|
||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||
@ -241,68 +254,18 @@ jobs:
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
|
||||
# The json is the same for all platforms, but we need to save one
|
||||
- name: Upload plugin json
|
||||
if: startsWith(matrix.config.os, 'ubuntu')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||
path: ./build/build/${{ env.PLUGIN_NAME }}.json
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
|
||||
- name: Run unit tests
|
||||
if: startsWith(matrix.config.os, 'ubuntu')
|
||||
run: |
|
||||
xvfb-run ./build/build/test/QodeAssistTest
|
||||
|
||||
update_json:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Download the JSON file
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||
path: ./${{ env.PLUGIN_NAME }}-origin
|
||||
|
||||
- name: Store Release upload_url
|
||||
run: |
|
||||
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
|
||||
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
|
||||
|
||||
- name: Run the Node.js script to update JSON
|
||||
env:
|
||||
QT_TOKEN: ${{ secrets.TOKEN }}
|
||||
API_URL: ${{ secrets.API_URL }}
|
||||
run: |
|
||||
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
|
||||
|
||||
- name: Delete previous json artifacts
|
||||
uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: ${{ env.PLUGIN_NAME }}*-json
|
||||
|
||||
- name: Upload the modified JSON file as an artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-json
|
||||
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
|
||||
|
||||
release:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, update_json]
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
|
||||
24
.github/workflows/check_formatting.yml
vendored
@ -1,24 +0,0 @@
|
||||
name: Check formatting
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-19
|
||||
- name: Check formatting
|
||||
run: |
|
||||
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
|
||||
clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
|
||||
git diff --exit-code || exit 1
|
||||
@ -11,9 +11,11 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
|
||||
find_package(GTest)
|
||||
|
||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
||||
|
||||
# IDE_VERSION is defined by QtCreator package
|
||||
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
||||
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
|
||||
@ -73,7 +75,7 @@ add_qtc_plugin(QodeAssist
|
||||
templates/StarCoder2Fim.hpp
|
||||
# templates/DeepSeekCoderFim.hpp
|
||||
# templates/CustomFimTemplate.hpp
|
||||
templates/Qwen.hpp
|
||||
templates/Qwen25CoderFIM.hpp
|
||||
templates/OpenAICompatible.hpp
|
||||
templates/Llama3.hpp
|
||||
templates/ChatML.hpp
|
||||
@ -82,6 +84,7 @@ add_qtc_plugin(QodeAssist
|
||||
templates/CodeLlamaQMLFim.hpp
|
||||
templates/GoogleAI.hpp
|
||||
templates/LlamaCppFim.hpp
|
||||
templates/Qwen3CoderFIM.hpp
|
||||
providers/Providers.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
@ -126,3 +129,10 @@ if (QtCreatorExecutable)
|
||||
)
|
||||
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
|
||||
endif()
|
||||
|
||||
#TODO change to TS_OUTPUT_DIRECTORY after removing Qt6.8
|
||||
qt_add_translations(TARGETS QodeAssist
|
||||
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
|
||||
RESOURCE_PREFIX "/translations"
|
||||
LUPDATE_OPTIONS -no-obsolete
|
||||
)
|
||||
|
||||
@ -17,6 +17,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
qml/parts/TopBar.qml
|
||||
qml/parts/BottomBar.qml
|
||||
qml/parts/AttachedFilesPlace.qml
|
||||
|
||||
RESOURCES
|
||||
icons/attach-file-light.svg
|
||||
icons/attach-file-dark.svg
|
||||
@ -24,6 +25,14 @@ qt_add_qml_module(QodeAssistChatView
|
||||
icons/close-light.svg
|
||||
icons/link-file-light.svg
|
||||
icons/link-file-dark.svg
|
||||
icons/load-chat-dark.svg
|
||||
icons/save-chat-dark.svg
|
||||
icons/clean-icon-dark.svg
|
||||
icons/file-in-system.svg
|
||||
icons/window-lock.svg
|
||||
icons/window-unlock.svg
|
||||
icons/chat-icon.svg
|
||||
icons/chat-pause-icon.svg
|
||||
SOURCES
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
@ -32,6 +41,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
ChatView.hpp ChatView.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
|
||||
@ -124,6 +124,7 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
bool foundCodeBlock = blockMatches.hasNext();
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
@ -140,7 +141,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
if (!remainingText.isEmpty()) {
|
||||
|
||||
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
||||
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
|
||||
|
||||
if (unclosedMatch.hasMatch()) {
|
||||
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
|
||||
if (!beforeCodeBlock.isEmpty()) {
|
||||
parts.append({MessagePart::Text, beforeCodeBlock, ""});
|
||||
}
|
||||
|
||||
parts.append(
|
||||
{MessagePart::Code, unclosedMatch.captured(2).trimmed(), unclosedMatch.captured(1)});
|
||||
} else if (!remainingText.isEmpty()) {
|
||||
parts.append({MessagePart::Text, remainingText, ""});
|
||||
}
|
||||
}
|
||||
@ -202,7 +215,7 @@ void ChatModel::resetModelTo(int index)
|
||||
if (index < 0 || index >= m_messages.size())
|
||||
return;
|
||||
|
||||
if (index < m_messages.size() - 1) {
|
||||
if (index < m_messages.size()) {
|
||||
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
||||
m_messages.remove(index, m_messages.size() - index);
|
||||
endRemoveRows();
|
||||
|
||||
@ -66,6 +66,10 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::autosave);
|
||||
|
||||
connect(m_clientInterface, &ClientInterface::messageReceivedCompletely, this, [this]() {
|
||||
this->setRequestProgressStatus(false);
|
||||
});
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageReceivedCompletely,
|
||||
@ -102,6 +106,31 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().textFontFamily,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::textFamilyChanged);
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().codeFontFamily,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::codeFamilyChanged);
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().textFontSize,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::textFontSizeChanged);
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().codeFontSize,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::codeFontSizeChanged);
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().textFormat,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::textFormatChanged);
|
||||
|
||||
updateInputTokensCount();
|
||||
}
|
||||
@ -131,6 +160,7 @@ void ChatRootView::sendMessage(const QString &message)
|
||||
|
||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||
clearAttachmentFiles();
|
||||
setRequestProgressStatus(true);
|
||||
}
|
||||
|
||||
void ChatRootView::copyToClipboard(const QString &text)
|
||||
@ -141,6 +171,7 @@ void ChatRootView::copyToClipboard(const QString &text)
|
||||
void ChatRootView::cancelRequest()
|
||||
{
|
||||
m_clientInterface->cancelRequest();
|
||||
setRequestProgressStatus(false);
|
||||
}
|
||||
|
||||
void ChatRootView::clearAttachmentFiles()
|
||||
@ -552,4 +583,42 @@ bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ChatRootView::textFontFamily() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().textFontFamily.stringValue();
|
||||
}
|
||||
|
||||
QString ChatRootView::codeFontFamily() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().codeFontFamily.stringValue();
|
||||
}
|
||||
|
||||
int ChatRootView::codeFontSize() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().codeFontSize();
|
||||
}
|
||||
|
||||
int ChatRootView::textFontSize() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().textFontSize();
|
||||
}
|
||||
|
||||
int ChatRootView::textFormat() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().textFormat();
|
||||
}
|
||||
|
||||
bool ChatRootView::isRequestInProgress() const
|
||||
{
|
||||
return m_isRequestInProgress;
|
||||
}
|
||||
|
||||
void ChatRootView::setRequestProgressStatus(bool state)
|
||||
{
|
||||
if (m_isRequestInProgress == state)
|
||||
return;
|
||||
m_isRequestInProgress = state;
|
||||
emit isRequestInProgressChanged();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -38,6 +38,13 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
|
||||
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
|
||||
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
|
||||
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
||||
Q_PROPERTY(
|
||||
bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
@ -80,6 +87,16 @@ public:
|
||||
void setRecentFilePath(const QString &filePath);
|
||||
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
|
||||
|
||||
QString textFontFamily() const;
|
||||
QString codeFontFamily() const;
|
||||
|
||||
int codeFontSize() const;
|
||||
int textFontSize() const;
|
||||
int textFormat() const;
|
||||
|
||||
bool isRequestInProgress() const;
|
||||
void setRequestProgressStatus(bool state);
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
@ -95,6 +112,13 @@ signals:
|
||||
void inputTokensCountChanged();
|
||||
void isSyncOpenFilesChanged();
|
||||
void chatFileNameChanged();
|
||||
void textFamilyChanged();
|
||||
void codeFamilyChanged();
|
||||
void codeFontSizeChanged();
|
||||
void textFontSizeChanged();
|
||||
void textFormatChanged();
|
||||
void chatRequestStarted();
|
||||
void isRequestInProgressChanged();
|
||||
|
||||
private:
|
||||
QString getChatsHistoryDir() const;
|
||||
@ -111,6 +135,7 @@ private:
|
||||
int m_inputTokensCount{0};
|
||||
bool m_isSyncOpenFiles;
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -29,4 +29,40 @@ void ChatUtils::copyToClipboard(const QString &text)
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
QString ChatUtils::getSafeMarkdownText(const QString &text) const
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
bool needsSanitization = false;
|
||||
for (const QChar &ch : text) {
|
||||
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
|
||||
needsSanitization = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsSanitization) {
|
||||
return text;
|
||||
}
|
||||
|
||||
QString safeText;
|
||||
safeText.reserve(text.size());
|
||||
|
||||
for (QChar ch : text) {
|
||||
if (ch.isNull()) {
|
||||
safeText.append(' ');
|
||||
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
||||
safeText.append(ch);
|
||||
} else if (ch.isPrint()) {
|
||||
safeText.append(ch);
|
||||
} else {
|
||||
safeText.append(QChar(0xFFFD));
|
||||
}
|
||||
}
|
||||
|
||||
return safeText;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@ -34,6 +34,7 @@ public:
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
106
ChatView/ChatView.cpp
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatView.hpp"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QSettings>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
namespace {
|
||||
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
||||
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
||||
| Qt::WindowCloseButtonHint;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatView::ChatView()
|
||||
: m_isPin(false)
|
||||
{
|
||||
setTitle("QodeAssist Chat");
|
||||
engine()->rootContext()->setContextProperty("_chatview", this);
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickView::SizeRootObjectToView);
|
||||
setMinimumSize({400, 300});
|
||||
setFlags(baseFlags);
|
||||
|
||||
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
|
||||
m_closeShortcut = new QShortcut(action->keySequence(), this);
|
||||
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
|
||||
|
||||
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
|
||||
if (m_closeShortcut) {
|
||||
m_closeShortcut->setKey(action->keySequence());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restoreSettings();
|
||||
}
|
||||
|
||||
void ChatView::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
saveSettings();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ChatView::saveSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("QodeAssist/ChatView/geometry", geometry());
|
||||
settings.setValue("QodeAssist/ChatView/pinned", m_isPin);
|
||||
}
|
||||
|
||||
void ChatView::restoreSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
const QRect savedGeometry
|
||||
= settings.value("QodeAssist/ChatView/geometry", QRect(100, 100, 800, 600)).toRect();
|
||||
setGeometry(savedGeometry);
|
||||
|
||||
const bool pinned = settings.value("QodeAssist/ChatView/pinned", false).toBool();
|
||||
setIsPin(pinned);
|
||||
}
|
||||
|
||||
bool ChatView::isPin() const
|
||||
{
|
||||
return m_isPin;
|
||||
}
|
||||
|
||||
void ChatView::setIsPin(bool newIsPin)
|
||||
{
|
||||
if (m_isPin == newIsPin)
|
||||
return;
|
||||
m_isPin = newIsPin;
|
||||
|
||||
if (m_isPin) {
|
||||
setFlags(baseFlags | Qt::WindowStaysOnTopHint);
|
||||
} else {
|
||||
setFlags(baseFlags);
|
||||
}
|
||||
|
||||
emit isPinChanged();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
51
ChatView/ChatView.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickView>
|
||||
#include <QShortcut>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatView : public QQuickView
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
||||
public:
|
||||
ChatView();
|
||||
|
||||
bool isPin() const;
|
||||
void setIsPin(bool newIsPin);
|
||||
|
||||
signals:
|
||||
void isPinChanged();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
void saveSettings();
|
||||
void restoreSettings();
|
||||
|
||||
bool m_isPin;
|
||||
QShortcut *m_closeShortcut;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
10
ChatView/icons/chat-icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_6)">
|
||||
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_6">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 523 B |
10
ChatView/icons/chat-pause-icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_17)">
|
||||
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8ZM8.4 6H15.6V13.2H8.4V6Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_17">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 548 B |
8
ChatView/icons/clean-icon-dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.75 15H3.25C2.00736 15 1 16.0074 1 17.25V39.75C1 40.9926 2.00736 42 3.25 42H16.75C17.9926 42 19 40.9926 19 39.75V17.25C19 16.0074 17.9926 15 16.75 15Z" stroke="black" stroke-width="2"/>
|
||||
<path d="M1.04316 11.015L18.9554 8.90787" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7.19462 10.363L7.02032 8.59516C6.92446 7.62284 8.18688 6.64116 9.8257 6.41365C11.4645 6.18615 12.8838 6.79555 12.9797 7.76787L13.154 9.53573" stroke="black" stroke-width="2"/>
|
||||
<path d="M6 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M10 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M14 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
12
ChatView/icons/file-in-system.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_59_114)">
|
||||
<path d="M2 8H12L16 4H40C42 4 44 6 44 8V36C44 38 42 40 40 40H6C4 40 2 38 2 36V8Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="3"/>
|
||||
<path d="M25 37C32.732 37 39 30.732 39 23C39 15.268 32.732 9 25 9C17.268 9 11 15.268 11 23C11 30.732 17.268 37 25 37Z" stroke="black" stroke-width="4"/>
|
||||
<path d="M33 35L42 44" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_59_114">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 624 B |
5
ChatView/icons/load-chat-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M10 16V36" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M5 21L10 16L15 21" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
5
ChatView/icons/save-chat-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 8V28" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M5 23L10 28L15 23" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 36H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
5
ChatView/icons/window-lock.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="4"/>
|
||||
<path d="M14 18V10C14 5.6 17.6 2 22 2C26.4 2 30 5.6 30 10V18" stroke="black" stroke-width="4"/>
|
||||
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 552 B |
5
ChatView/icons/window-unlock.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="4"/>
|
||||
<path d="M14 17V9.5C14 5.375 17.15 2 21 2C24.85 2 27.5 2.875 27.5 7" stroke="black" stroke-width="4"/>
|
||||
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 559 B |
@ -23,6 +23,7 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property alias text: badgeText.text
|
||||
property alias hovered: mouse.hovered
|
||||
|
||||
implicitWidth: badgeText.implicitWidth + root.radius
|
||||
implicitHeight: badgeText.implicitHeight + 6
|
||||
@ -37,4 +38,10 @@ Rectangle {
|
||||
anchors.centerIn: parent
|
||||
color: palette.buttonText
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,8 +27,26 @@ Rectangle {
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property alias messageAttachments: attachmentsModel.model
|
||||
property string textFontFamily: Qt.application.font.family
|
||||
property string codeFontFamily: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
}
|
||||
}
|
||||
property int textFontSize: Qt.application.font.pointSize
|
||||
property int codeFontSize: Qt.application.font.pointSize
|
||||
property int textFormat: 0
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
property real listViewContentY: 0
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
|
||||
@ -84,6 +102,8 @@ Rectangle {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
blockStart: root.y + msgCreatorDelegate.y
|
||||
currentContentY: root.listViewContentY
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,8 +160,7 @@ Rectangle {
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 2
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
text: qsTr("ResetTo")
|
||||
@ -156,11 +175,28 @@ Rectangle {
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: itemData.text
|
||||
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||
: itemData.text
|
||||
font.family: root.textFontFamily
|
||||
font.pointSize: root.textFontSize
|
||||
textFormat: {
|
||||
if (root.textFormat == 0) {
|
||||
return Text.MarkdownText
|
||||
} else if (root.textFormat == 1) {
|
||||
return Text.RichText
|
||||
} else {
|
||||
return Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
component CodeBlockComponent : CodeBlock {
|
||||
id: codeblock
|
||||
|
||||
required property var itemData
|
||||
anchors {
|
||||
left: parent.left
|
||||
@ -171,5 +207,7 @@ Rectangle {
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,12 +70,17 @@ ChatRootView {
|
||||
loadButton.onClicked: root.showLoadDialog()
|
||||
clearButton.onClicked: root.clearChat()
|
||||
tokensBadge {
|
||||
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
recentPath {
|
||||
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||
pinButton {
|
||||
visible: typeof _chatview !== 'undefined'
|
||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
@ -99,6 +104,12 @@ ChatRootView {
|
||||
messageAttachments: model.attachments
|
||||
isUserMessage: model.roleType === ChatModel.User
|
||||
messageIndex: index
|
||||
listViewContentY: chatListView.contentY
|
||||
textFontFamily: root.textFontFamily
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
textFontSize: root.textFontSize
|
||||
textFormat: root.textFormat
|
||||
|
||||
onResetChatToMessage: function(index) {
|
||||
messageInput.text = model.content
|
||||
@ -197,8 +208,9 @@ ChatRootView {
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
sendButton.onClicked: root.sendChatMessage()
|
||||
stopButton.onClicked: root.cancelRequest()
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: root.cancelRequest()
|
||||
isRequestInProgress: root.isRequestInProgress
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
@ -223,4 +235,8 @@ ChatRootView {
|
||||
messageInput.text = ""
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
messageInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,17 +27,25 @@ Rectangle {
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
|
||||
readonly property string monospaceFont: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
property real currentContentY: 0
|
||||
property real blockStart: 0
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
|
||||
readonly property real buttonTopMargin: 5
|
||||
readonly property real blockEnd: blockStart + root.height
|
||||
readonly property real maxButtonOffset: Math.max(0, root.height - copyButton.height - buttonTopMargin)
|
||||
|
||||
readonly property real buttonPosition: {
|
||||
if (currentContentY > blockEnd) {
|
||||
return buttonTopMargin;
|
||||
}
|
||||
else if (currentContentY > blockStart) {
|
||||
let offset = currentContentY - blockStart;
|
||||
return Math.min(offset, maxButtonOffset);
|
||||
}
|
||||
return buttonTopMargin;
|
||||
}
|
||||
|
||||
color: palette.alternateBase
|
||||
@ -45,7 +53,6 @@ Rectangle {
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
radius: 4
|
||||
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeText.implicitHeight + 20
|
||||
|
||||
@ -55,14 +62,11 @@ Rectangle {
|
||||
|
||||
TextEdit {
|
||||
id: codeText
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
font.family: root.monospaceFont
|
||||
font.pointSize: Qt.application.font.pointSize
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
@ -77,14 +81,20 @@ Rectangle {
|
||||
text: root.language
|
||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||
: Qt.lighter(root.color, 1.1)
|
||||
font.pointSize: 8
|
||||
font.pointSize: codeText.font.pointSize - 4
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 5
|
||||
text: "Copy"
|
||||
id: copyButton
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: root.buttonPosition
|
||||
right: parent.right
|
||||
rightMargin: root.buttonTopMargin
|
||||
}
|
||||
|
||||
text: qsTr("Copy")
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
|
||||
@ -25,7 +25,6 @@ TextEdit {
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
}
|
||||
|
||||
@ -26,11 +26,12 @@ Rectangle {
|
||||
id: root
|
||||
|
||||
property alias sendButton: sendButtonId
|
||||
property alias stopButton: stopButtonId
|
||||
property alias syncOpenFiles: syncOpenFilesId
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias linkFiles: linkFilesId
|
||||
|
||||
property bool isRequestInProgress: false
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
@ -51,13 +52,16 @@ Rectangle {
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
text: qsTr("Send")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: stopButtonId
|
||||
|
||||
text: qsTr("Stop")
|
||||
icon {
|
||||
source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM")
|
||||
: qsTr("Stop")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
@ -68,7 +72,9 @@ Rectangle {
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
text: qsTr("Attach files")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Attach file to message")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
@ -79,7 +85,9 @@ Rectangle {
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
text: qsTr("Link files")
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Link file to context")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
|
||||
Rectangle {
|
||||
@ -30,6 +31,7 @@ Rectangle {
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
property alias recentPath: recentPathId
|
||||
property alias openChatHistory: openChatHistoryId
|
||||
property alias pinButton: pinButtonId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
@ -46,22 +48,61 @@ Rectangle {
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: pinButtonId
|
||||
|
||||
checkable: true
|
||||
|
||||
icon {
|
||||
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: checked ? qsTr("Unpin chat window")
|
||||
: qsTr("Pin chat window to the top")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: saveButtonId
|
||||
|
||||
text: qsTr("Save")
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Save chat to *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: loadButtonId
|
||||
|
||||
text: qsTr("Load")
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Load chat from *.json file")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: clearButtonId
|
||||
|
||||
text: qsTr("Clear")
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Clean chat")
|
||||
}
|
||||
|
||||
Text {
|
||||
@ -74,7 +115,14 @@ Rectangle {
|
||||
QoAButton {
|
||||
id: openChatHistoryId
|
||||
|
||||
text: qsTr("Show in system")
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Show in system")
|
||||
}
|
||||
|
||||
Item {
|
||||
@ -83,6 +131,10 @@ Rectangle {
|
||||
|
||||
Badge {
|
||||
id: tokensBadgeId
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +99,19 @@ const QVector<LanguageProperties> &getKnownLanguages()
|
||||
return knownLanguages;
|
||||
}
|
||||
|
||||
bool CodeHandler::hasCodeBlocks(const QString &text)
|
||||
{
|
||||
QStringList lines = text.split('\n');
|
||||
|
||||
for (const QString &line : lines) {
|
||||
if (line.trimmed().startsWith("```")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
|
||||
{
|
||||
QHash<QString, QString> result;
|
||||
|
||||
@ -40,6 +40,11 @@ public:
|
||||
*/
|
||||
static QString detectLanguageFromExtension(const QString &extension);
|
||||
|
||||
/**
|
||||
* Detects if text contains code blocks, or returns false if this was not possible
|
||||
*/
|
||||
static bool hasCodeBlocks(const QString &text);
|
||||
|
||||
private:
|
||||
static QString getCommentPrefix(const QString &language);
|
||||
|
||||
|
||||
@ -57,6 +57,17 @@ LLMClientInterface::LLMClientInterface(
|
||||
&LLMCore::RequestHandler::completionReceived,
|
||||
this,
|
||||
&LLMClientInterface::sendCompletionToClient);
|
||||
|
||||
// TODO handle error
|
||||
// connect(
|
||||
// &m_requestHandler,
|
||||
// &LLMCore::RequestHandler::requestFinished,
|
||||
// this,
|
||||
// [this](const QString &, bool success, const QString &errorString) {
|
||||
// if (!success) {
|
||||
// emit error(errorString);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||
@ -207,10 +218,8 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
: QString{"generateContent?"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} else {
|
||||
config.url = QUrl(QString("%1%2").arg(
|
||||
url,
|
||||
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint()));
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
|
||||
}
|
||||
config.apiKey = provider->apiKey();
|
||||
@ -289,6 +298,26 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::endpoint(
|
||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||
{
|
||||
QString endpoint;
|
||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
||||
: m_generalSettings.ccEndpointMode.stringValue();
|
||||
if (endpointMode == "Auto") {
|
||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint();
|
||||
} else if (endpointMode == "Custom") {
|
||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
} else if (endpointMode == "FIM") {
|
||||
endpoint = provider->completionEndpoint();
|
||||
} else if (endpointMode == "Chat") {
|
||||
endpoint = provider->chatEndpoint();
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
@ -318,16 +347,28 @@ void LLMClientInterface::sendCompletionToClient(
|
||||
|
||||
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
|
||||
|
||||
QString processedCompletion
|
||||
= promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||
&& m_completeSettings.smartProcessInstuctText()
|
||||
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
|
||||
: completion;
|
||||
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
|
||||
QString processedCompletion;
|
||||
|
||||
if (outputHandler == "Raw text") {
|
||||
processedCompletion = completion;
|
||||
} else if (outputHandler == "Force processing") {
|
||||
processedCompletion = CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(request));
|
||||
} else { // "Auto"
|
||||
processedCompletion = CodeHandler::hasCodeBlocks(completion)
|
||||
? CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(
|
||||
request))
|
||||
: completion;
|
||||
}
|
||||
|
||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||
QJsonObject range;
|
||||
range["start"] = position;
|
||||
range["end"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
completions.append(completionItem);
|
||||
|
||||
@ -77,6 +77,7 @@ private:
|
||||
|
||||
LLMCore::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
|
||||
@ -29,6 +29,36 @@
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
|
||||
{
|
||||
if (suggestion.isEmpty() || rightText.isEmpty()) {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
int j = 0;
|
||||
QString processed = rightText;
|
||||
QSet<int> matchedPositions;
|
||||
|
||||
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
|
||||
if (suggestion[i] == processed[j]) {
|
||||
matchedPositions.insert(j);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedPositions.isEmpty()) {
|
||||
return suggestion + rightText;
|
||||
}
|
||||
|
||||
QList<int> positions = matchedPositions.values();
|
||||
std::sort(positions.begin(), positions.end(), std::greater<int>());
|
||||
for (int pos : positions) {
|
||||
processed.remove(pos, 1);
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
LLMSuggestion::LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||
@ -43,16 +73,23 @@ LLMSuggestion::LLMSuggestion(
|
||||
|
||||
QTextCursor cursor(sourceDocument);
|
||||
cursor.setPosition(startPos);
|
||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||
|
||||
QTextBlock block = cursor.block();
|
||||
QString blockText = block.text();
|
||||
|
||||
int startPosInBlock = startPos - block.position();
|
||||
int endPosInBlock = endPos - block.position();
|
||||
int cursorPositionInBlock = cursor.positionInBlock();
|
||||
|
||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
|
||||
replacementDocument()->setPlainText(blockText);
|
||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||
|
||||
if (!data.text.contains('\n')) {
|
||||
QString processedRightText = mergeWithRightText(data.text, rightText);
|
||||
processedRightText = processedRightText.mid(data.text.length());
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text
|
||||
+ processedRightText;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
} else {
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
}
|
||||
}
|
||||
|
||||
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
||||
@ -77,31 +114,87 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
|
||||
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||
|
||||
if (next == -1)
|
||||
return apply();
|
||||
if (next == -1) {
|
||||
if (part == Line) {
|
||||
next = text.length();
|
||||
} else {
|
||||
return apply();
|
||||
}
|
||||
}
|
||||
|
||||
if (part == Line)
|
||||
++next;
|
||||
|
||||
QString subText = text.mid(startPos, next - startPos);
|
||||
if (subText.isEmpty())
|
||||
|
||||
if (subText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentCursor.insertText(subText);
|
||||
if (!subText.contains('\n')) {
|
||||
currentCursor.insertText(subText);
|
||||
|
||||
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||
if (!newCompletionText.isEmpty()) {
|
||||
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||
const QString remainingText = text.mid(next);
|
||||
if (!remainingText.isEmpty()) {
|
||||
QTextCursor newCursor = widget->textCursor();
|
||||
const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument(
|
||||
newCursor.document(), newCursor.position());
|
||||
const Utils::Text::Position
|
||||
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
|
||||
newEnd{newStart.line, newStart.column + int(remainingText.length())};
|
||||
const Utils::Text::Range newRange{newStart, newEnd};
|
||||
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
|
||||
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
|
||||
widget->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
||||
}
|
||||
} else {
|
||||
currentCursor.insertText(subText);
|
||||
|
||||
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||
if (!newCompletionText.isEmpty()) {
|
||||
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
|
||||
const Utils::Text::Range newRange{newStart, newEnd};
|
||||
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
|
||||
widget->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LLMSuggestion::apply()
|
||||
{
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||
|
||||
QTextCursor editCursor = cursor;
|
||||
|
||||
int firstLineEnd = text.indexOf('\n');
|
||||
if (firstLineEnd != -1) {
|
||||
QString firstLine = text.left(firstLineEnd);
|
||||
QString restOfText = text.mid(firstLineEnd);
|
||||
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
|
||||
editCursor.insertText(mergedFirstLine + restOfText);
|
||||
} else {
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedText = mergeWithRightText(text, textAfterCursor);
|
||||
editCursor.insertText(mergedText);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -40,5 +40,6 @@ public:
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
bool apply() override;
|
||||
};
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.5.9",
|
||||
"Version" : "0.6.2",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
|
||||
@ -8,5 +8,7 @@
|
||||
<file>resources/images/improve-current-code-icon.png</file>
|
||||
<file>resources/images/suggest-new-icon.png</file>
|
||||
<file>resources/images/suggest-new-icon@2x.png</file>
|
||||
<file>resources/images/qode-assist-chat-icon.png</file>
|
||||
<file>resources/images/qode-assist-chat-icon@2x.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -24,6 +24,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LSPCompletion.hpp"
|
||||
#include "QuickRefactorHandler.hpp"
|
||||
@ -37,6 +39,7 @@ namespace QodeAssist {
|
||||
|
||||
class QodeAssistClient : public LanguageClient::Client
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||
~QodeAssistClient() override;
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="en_001"></TS>
|
||||
75
README.md
@ -2,7 +2,8 @@
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
|
||||
 QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
|
||||
@ -23,10 +24,12 @@
|
||||
7. [Configure for Ollama](#configure-for-ollama)
|
||||
8. [Configure for llama.cpp](#configure-for-llamacpp)
|
||||
9. [System Prompt Configuration](#system-prompt-configuration)
|
||||
10. [File Context Features](#file-context-features)
|
||||
11. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
12. [Development Progress](#development-progress)
|
||||
13. [Hotkeys](#hotkeys)
|
||||
10. [File Context Feature](#file-context-feature)
|
||||
11. [Quick Refactoring Feature](#quick-refactoring-feature)
|
||||
12. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
13. [Development Progress](#development-progress)
|
||||
14. [Hotkeys](#hotkeys)
|
||||
15. [Ignoring Files](#ignoring-files)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
15. [Support the Development](#support-the-development-of-qodeassist)
|
||||
16. [How to Build](#how-to-build)
|
||||
@ -37,7 +40,8 @@
|
||||
- Sharing IDE opened files with model context (disabled by default, need enable in settings)
|
||||
- Quick refactor code via fast chat command and opened files
|
||||
- Chat functionality:
|
||||
- Side and Bottom panels
|
||||
- Side and Bottom panels(enabling in chat settings due stability reason with QQuickWidget problem)
|
||||
- Chat in additional popup window with pinning(recommended)
|
||||
- Chat history autosave and restore
|
||||
- Token usage monitoring and management
|
||||
- Attach files for one-time code analysis
|
||||
@ -82,6 +86,11 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Chat in addtional window: (click to expand)</summary>
|
||||
<img width="851" height="865" alt="image" src="https://github.com/user-attachments/assets/a68894b7-886e-4501-a61b-7161ae34b427" />
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Automatic syncing with open editor files: (click to expand)</summary>
|
||||
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
|
||||
@ -201,7 +210,7 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
|
||||
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
|
||||
|
||||
## File Context Features
|
||||
## File Context Feature
|
||||
|
||||
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant.
|
||||
|
||||
@ -234,9 +243,23 @@ Linked files provide persistent context throughout the conversation:
|
||||
- Supports automatic syncing with open editor files (can be enabled in settings)
|
||||
- Files can be added/removed at any time during the conversation
|
||||
|
||||
## Quick Refactoring Feature
|
||||
### Setup
|
||||
Since this is actually a small chat with redirected output, the main settings of the provider, model and template are taken from the chat settings
|
||||
### Using
|
||||
The request to model consist of instructions to model, selection code and cursor position
|
||||
The default instruction is: "Refactor the code to improve its quality and maintainability." and sending if text field is empty
|
||||
Also there buttons to quick call instractions:
|
||||
* Repeat latest instruction, will activate after sending first request in QtCreator session
|
||||
* Improve current selection code
|
||||
* Suggestion alternative variant of selection code
|
||||
* Other instructions[TBD]
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
- QtCreator 16.0.1 - 0.5.7 - 0.x.x
|
||||
- QtCreator 17.0.0 - 0.6.0 - 0.x.x
|
||||
- QtCreator 16.0.2 - 0.5.13 - 0.x.x
|
||||
- QtCreator 16.0.1 - 0.5.7 - 0.5.13
|
||||
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
|
||||
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
|
||||
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
|
||||
@ -251,9 +274,19 @@ Linked files provide persistent context throughout the conversation:
|
||||
- [x] Sharing diff with model
|
||||
- [ ] Sharing project source with model
|
||||
- [ ] Support for more providers and models
|
||||
- [ ] Support MCP
|
||||
|
||||
## Hotkeys
|
||||
|
||||
All hotkeys available in QtCreator Settings
|
||||
Also you can find default hotkeys here:
|
||||
- To call chat with llm in separate window, you can use:
|
||||
- on Mac: Option + Command + W
|
||||
- on Windows: Ctrl + Alt + W
|
||||
- on Linux: Ctrl + Alt + W
|
||||
- To close chat with llm in separate window, you can use:
|
||||
- on Mac: Option + Command + S
|
||||
- on Windows: Ctrl + Alt + S
|
||||
- on Linux: Ctrl + Alt + S
|
||||
- To call manual request to suggestion, you can use or change it in settings
|
||||
- on Mac: Option + Command + Q
|
||||
- on Windows: Ctrl + Alt + Q
|
||||
@ -264,6 +297,30 @@ Linked files provide persistent context throughout the conversation:
|
||||
- on Mac: Option + Command + R
|
||||
- on Windows: Ctrl + Alt + R
|
||||
- on Linux with KDE Plasma: Ctrl + Alt + R
|
||||
|
||||
## Ignoring Files
|
||||
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
|
||||
|
||||
### How to Use .qodeassistignore
|
||||
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
|
||||
- Add patterns for files and directories that should be excluded from the context.
|
||||
- QodeAssist will automatically detect this file and apply the exclusion rules.
|
||||
|
||||
### .qodeassistignore File Format
|
||||
The file format is similar to .gitignore:
|
||||
- Each pattern is written on a separate line
|
||||
- Empty lines are ignored
|
||||
- Lines starting with # are considered comments
|
||||
- Standard wildcards work the same as in .gitignore
|
||||
- To negate a pattern, use ! at the beginning of the line
|
||||
```
|
||||
# Ignore all files in the build directory
|
||||
/build
|
||||
*.tmp
|
||||
# Ignore a specific file
|
||||
src/generated/autogen.cpp
|
||||
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
14
TaskFlow/CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(Editor)
|
||||
# add_subdirectory(serialization)
|
||||
# add_subdirectory(tasks)
|
||||
|
||||
qt_add_library(TaskFlow STATIC)
|
||||
|
||||
target_link_libraries(TaskFlow
|
||||
PUBLIC
|
||||
TaskFlowCore
|
||||
TaskFlowEditorplugin
|
||||
# TaskFlowSerialization
|
||||
# TaskFlowTasks
|
||||
)
|
||||
41
TaskFlow/Editor/CMakeLists.txt
Normal file
@ -0,0 +1,41 @@
|
||||
qt_add_library(TaskFlowEditor STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(TaskFlowEditor
|
||||
URI TaskFlow.Editor
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
RESOURCES
|
||||
QML_FILES
|
||||
qml/FlowEditorView.qml
|
||||
qml/Flow.qml
|
||||
qml/Task.qml
|
||||
qml/TaskPort.qml
|
||||
qml/TaskParameter.qml
|
||||
qml/TaskConnection.qml
|
||||
SOURCES
|
||||
FlowEditor.hpp FlowEditor.cpp
|
||||
FlowsModel.hpp FlowsModel.cpp
|
||||
TaskItem.hpp TaskItem.cpp
|
||||
FlowItem.hpp FlowItem.cpp
|
||||
TaskModel.hpp TaskModel.cpp
|
||||
TaskPortItem.hpp TaskPortItem.cpp
|
||||
TaskPortModel.hpp TaskPortModel.cpp
|
||||
TaskConnectionsModel.hpp TaskConnectionsModel.cpp
|
||||
TaskConnectionItem.hpp TaskConnectionItem.cpp
|
||||
GridBackground.hpp GridBackground.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(TaskFlowEditor
|
||||
PUBLIC
|
||||
Qt::Quick
|
||||
PRIVATE
|
||||
TaskFlowCore
|
||||
)
|
||||
|
||||
target_include_directories(TaskFlowEditor
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
)
|
||||
120
TaskFlow/Editor/FlowEditor.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
|
||||
#include "FlowEditor.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowEditor::FlowEditor(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{}
|
||||
|
||||
void FlowEditor::initialize()
|
||||
{
|
||||
emit availableTaskTypesChanged();
|
||||
emit availableFlowsChanged();
|
||||
|
||||
m_flowsModel = new FlowsModel(m_flowManager, this);
|
||||
|
||||
emit flowsModelChanged();
|
||||
|
||||
if (m_flowsModel->rowCount() > 0) {
|
||||
setCurrentFlowIndex(0);
|
||||
}
|
||||
|
||||
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
|
||||
m_currentFlow = m_flowManager->getFlow();
|
||||
emit currentFlowChanged();
|
||||
}
|
||||
|
||||
QString FlowEditor::currentFlowId() const
|
||||
{
|
||||
return m_currentFlowId;
|
||||
}
|
||||
|
||||
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
|
||||
{
|
||||
if (m_currentFlowId == newCurrentFlowId)
|
||||
return;
|
||||
m_currentFlowId = newCurrentFlowId;
|
||||
emit currentFlowIdChanged();
|
||||
}
|
||||
|
||||
QStringList FlowEditor::availableTaskTypes() const
|
||||
{
|
||||
if (m_flowManager)
|
||||
return m_flowManager->getAvailableTasksTypes();
|
||||
else {
|
||||
return {"No flow manager"};
|
||||
}
|
||||
}
|
||||
|
||||
QStringList FlowEditor::availableFlows() const
|
||||
{
|
||||
if (m_flowManager) {
|
||||
auto flows = m_flowManager->getAvailableFlows();
|
||||
return flows.size() > 0 ? flows : QStringList{"No flows"};
|
||||
} else {
|
||||
return {"No flow manager"};
|
||||
}
|
||||
}
|
||||
|
||||
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
|
||||
{
|
||||
if (m_flowManager == newFlowManager)
|
||||
return;
|
||||
m_flowManager = newFlowManager;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
FlowsModel *FlowEditor::flowsModel() const
|
||||
{
|
||||
return m_flowsModel;
|
||||
}
|
||||
|
||||
int FlowEditor::currentFlowIndex() const
|
||||
{
|
||||
return m_currentFlowIndex;
|
||||
}
|
||||
|
||||
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
|
||||
{
|
||||
if (m_currentFlowIndex == newCurrentFlowIndex)
|
||||
return;
|
||||
m_currentFlowIndex = newCurrentFlowIndex;
|
||||
emit currentFlowIndexChanged();
|
||||
}
|
||||
|
||||
Flow *FlowEditor::getFlow(const QString &flowName)
|
||||
{
|
||||
return m_flowManager->getFlow(flowName);
|
||||
}
|
||||
|
||||
Flow *FlowEditor::getCurrentFlow()
|
||||
{
|
||||
return m_flowManager->getFlow(m_currentFlowId);
|
||||
}
|
||||
|
||||
Flow *FlowEditor::currentFlow() const
|
||||
{
|
||||
return m_currentFlow;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
86
TaskFlow/Editor/FlowEditor.hpp
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "FlowsModel.hpp"
|
||||
#include <FlowManager.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowEditor : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(
|
||||
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
|
||||
Q_PROPERTY(
|
||||
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
|
||||
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
|
||||
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
|
||||
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
|
||||
currentFlowIndexChanged)
|
||||
|
||||
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
|
||||
|
||||
public:
|
||||
FlowEditor(QQuickItem *parent = nullptr);
|
||||
|
||||
void initialize();
|
||||
|
||||
QString currentFlowId() const;
|
||||
void setCurrentFlowId(const QString &newCurrentFlowId);
|
||||
|
||||
QStringList availableTaskTypes() const;
|
||||
QStringList availableFlows() const;
|
||||
|
||||
void setFlowManager(FlowManager *newFlowManager);
|
||||
|
||||
FlowsModel *flowsModel() const;
|
||||
|
||||
int currentFlowIndex() const;
|
||||
void setCurrentFlowIndex(int newCurrentFlowIndex);
|
||||
|
||||
Q_INVOKABLE Flow *getFlow(const QString &flowName);
|
||||
Q_INVOKABLE Flow *getCurrentFlow();
|
||||
|
||||
Flow *currentFlow() const;
|
||||
|
||||
signals:
|
||||
void currentFlowIdChanged();
|
||||
void availableTaskTypesChanged();
|
||||
void availableFlowsChanged();
|
||||
void flowsModelChanged();
|
||||
|
||||
void currentFlowIndexChanged();
|
||||
|
||||
void currentFlowChanged();
|
||||
|
||||
private:
|
||||
FlowManager *m_flowManager = nullptr;
|
||||
QString m_currentFlowId;
|
||||
FlowsModel *m_flowsModel;
|
||||
int m_currentFlowIndex;
|
||||
Flow *m_currentFlow = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
90
TaskFlow/Editor/FlowItem.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
#include "FlowItem.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowItem::FlowItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); });
|
||||
}
|
||||
|
||||
QString FlowItem::flowId() const
|
||||
{
|
||||
if (!m_flow)
|
||||
return {"no flow"};
|
||||
return m_flow->flowId();
|
||||
}
|
||||
|
||||
void FlowItem::setFlowId(const QString &newFlowId)
|
||||
{
|
||||
if (m_flow->flowId() == newFlowId)
|
||||
return;
|
||||
m_flow->setFlowId(newFlowId);
|
||||
emit flowIdChanged();
|
||||
}
|
||||
|
||||
Flow *FlowItem::flow() const
|
||||
{
|
||||
return m_flow;
|
||||
}
|
||||
|
||||
void FlowItem::setFlow(Flow *newFlow)
|
||||
{
|
||||
if (m_flow == newFlow)
|
||||
return;
|
||||
m_flow = newFlow;
|
||||
emit flowChanged();
|
||||
emit flowIdChanged();
|
||||
qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow;
|
||||
|
||||
m_taskModel = new TaskModel(m_flow, this);
|
||||
m_connectionsModel = new TaskConnectionsModel(m_flow, this);
|
||||
|
||||
emit taskModelChanged();
|
||||
emit connectionsModelChanged();
|
||||
}
|
||||
|
||||
TaskModel *FlowItem::taskModel() const
|
||||
{
|
||||
return m_taskModel;
|
||||
}
|
||||
|
||||
TaskConnectionsModel *FlowItem::connectionsModel() const
|
||||
{
|
||||
return m_connectionsModel;
|
||||
}
|
||||
|
||||
QVariantList FlowItem::taskItems() const
|
||||
{
|
||||
return m_taskItems;
|
||||
}
|
||||
|
||||
void FlowItem::setTaskItems(const QVariantList &newTaskItems)
|
||||
{
|
||||
qDebug() << "FlowItem::setTaskItems" << newTaskItems;
|
||||
if (m_taskItems == newTaskItems)
|
||||
return;
|
||||
m_taskItems = newTaskItems;
|
||||
emit taskItemsChanged();
|
||||
}
|
||||
|
||||
void FlowItem::updateFlowLayout()
|
||||
{
|
||||
auto allItems = this->childItems();
|
||||
|
||||
for (auto child : allItems) {
|
||||
if (child->objectName() == QString("TaskItem")) {
|
||||
qDebug() << "Found TaskItem:" << child;
|
||||
auto taskItem = qobject_cast<TaskItem *>(child);
|
||||
m_taskItemsList.insert(taskItem, taskItem->task());
|
||||
}
|
||||
|
||||
if (child->objectName() == QString("TaskConnectionItem")) {
|
||||
qDebug() << "Found TaskConnectionItem:" << child;
|
||||
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
|
||||
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
61
TaskFlow/Editor/FlowItem.hpp
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "TaskConnectionItem.hpp"
|
||||
#include "TaskConnectionsModel.hpp"
|
||||
#include "TaskItem.hpp"
|
||||
#include "TaskModel.hpp"
|
||||
#include <Flow.hpp>
|
||||
#include <TaskConnection.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged)
|
||||
Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged)
|
||||
Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged)
|
||||
Q_PROPERTY(
|
||||
TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged)
|
||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
||||
|
||||
public:
|
||||
explicit FlowItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QString flowId() const;
|
||||
void setFlowId(const QString &newFlowId);
|
||||
|
||||
Flow *flow() const;
|
||||
void setFlow(Flow *newFlow);
|
||||
|
||||
TaskModel *taskModel() const;
|
||||
|
||||
TaskConnectionsModel *connectionsModel() const;
|
||||
|
||||
QVariantList taskItems() const;
|
||||
void setTaskItems(const QVariantList &newTaskItems);
|
||||
|
||||
void updateFlowLayout();
|
||||
|
||||
signals:
|
||||
void flowIdChanged();
|
||||
void flowChanged();
|
||||
void taskModelChanged();
|
||||
void connectionsModelChanged();
|
||||
void taskItemsChanged();
|
||||
|
||||
private:
|
||||
Flow *m_flow = nullptr;
|
||||
TaskModel *m_taskModel = nullptr;
|
||||
TaskConnectionsModel *m_connectionsModel = nullptr;
|
||||
QVariantList m_taskItems;
|
||||
|
||||
QHash<TaskItem *, BaseTask *> m_taskItemsList;
|
||||
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
54
TaskFlow/Editor/FlowsModel.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "FlowsModel.hpp"
|
||||
|
||||
#include "FlowManager.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_flowManager(flowManager)
|
||||
{
|
||||
connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded);
|
||||
}
|
||||
|
||||
int FlowsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_flowManager->flows().size();
|
||||
}
|
||||
|
||||
QVariant FlowsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size())
|
||||
return QVariant();
|
||||
|
||||
const auto flows = m_flowManager->flows().values();
|
||||
|
||||
switch (role) {
|
||||
case FlowRoles::FlowIdRole:
|
||||
return flows.at(index.row())->flowId();
|
||||
case FlowRoles::FlowDataRole:
|
||||
return QVariant::fromValue(flows.at(index.row()));
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FlowsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[FlowRoles::FlowIdRole] = "flowId";
|
||||
roles[FlowRoles::FlowDataRole] = "flowData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void FlowsModel::onFlowAdded(const QString &flowId)
|
||||
{
|
||||
// qDebug() << "FlowsModel::Flow added: " << flowId;
|
||||
// int newIndex = m_flowManager->flows().size();
|
||||
// beginInsertRows(QModelIndex(), newIndex, newIndex);
|
||||
// endInsertRows();
|
||||
}
|
||||
|
||||
void FlowsModel::onFlowRemoved(const QString &flowId) {}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
31
TaskFlow/Editor/FlowsModel.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
|
||||
// #include "tasks/Flow.hpp"
|
||||
#include <FlowManager.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole };
|
||||
|
||||
FlowsModel(FlowManager *flowManager, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
public slots:
|
||||
void onFlowAdded(const QString &flowId);
|
||||
void onFlowRemoved(const QString &flowId);
|
||||
|
||||
private:
|
||||
FlowManager *m_flowManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
98
TaskFlow/Editor/GridBackground.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "GridBackground.hpp"
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QQuickWindow>
|
||||
#include <QSGSimpleRectNode>
|
||||
#include <QSGSimpleTextureNode>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
GridBackground::GridBackground(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setFlag(QQuickItem::ItemHasContents, true);
|
||||
}
|
||||
|
||||
int GridBackground::gridSize() const
|
||||
{
|
||||
return m_gridSize;
|
||||
}
|
||||
|
||||
void GridBackground::setGridSize(int size)
|
||||
{
|
||||
if (m_gridSize != size) {
|
||||
m_gridSize = size;
|
||||
update();
|
||||
emit gridSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor GridBackground::gridColor() const
|
||||
{
|
||||
return m_gridColor;
|
||||
}
|
||||
|
||||
void GridBackground::setGridColor(const QColor &color)
|
||||
{
|
||||
if (m_gridColor != color) {
|
||||
m_gridColor = color;
|
||||
update();
|
||||
emit gridColorChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
|
||||
{
|
||||
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
|
||||
if (!node) {
|
||||
node = new QSGSimpleTextureNode();
|
||||
}
|
||||
|
||||
QPixmap pixmap(width(), height());
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
QPen pen(m_gridColor);
|
||||
pen.setWidth(1);
|
||||
painter.setPen(pen);
|
||||
painter.setOpacity(this->opacity());
|
||||
|
||||
for (int x = 0; x < width(); x += m_gridSize) {
|
||||
painter.drawLine(x, 0, x, height());
|
||||
}
|
||||
|
||||
for (int y = 0; y < height(); y += m_gridSize) {
|
||||
painter.drawLine(0, y, width(), y);
|
||||
}
|
||||
|
||||
painter.end();
|
||||
|
||||
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
|
||||
node->setTexture(texture);
|
||||
node->setRect(boundingRect());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
57
TaskFlow/Editor/GridBackground.hpp
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QQuickItem>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class GridBackground : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
|
||||
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
|
||||
|
||||
public:
|
||||
explicit GridBackground(QQuickItem *parent = nullptr);
|
||||
|
||||
int gridSize() const;
|
||||
void setGridSize(int size);
|
||||
|
||||
QColor gridColor() const;
|
||||
void setGridColor(const QColor &color);
|
||||
|
||||
signals:
|
||||
void gridSizeChanged();
|
||||
void gridColorChanged();
|
||||
|
||||
protected:
|
||||
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
|
||||
|
||||
private:
|
||||
int m_gridSize = 20;
|
||||
QColor m_gridColor = QColor(128, 128, 128);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
153
TaskFlow/Editor/TaskConnectionItem.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
#include "TaskConnectionItem.hpp"
|
||||
#include "TaskItem.hpp"
|
||||
#include "TaskPortItem.hpp"
|
||||
#include <QDebug>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskConnectionItem::TaskConnectionItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setObjectName("TaskConnectionItem");
|
||||
}
|
||||
|
||||
void TaskConnectionItem::setConnection(TaskConnection *connection)
|
||||
{
|
||||
if (m_connection == connection)
|
||||
return;
|
||||
|
||||
m_connection = connection;
|
||||
emit connectionChanged();
|
||||
|
||||
calculatePositions();
|
||||
}
|
||||
|
||||
void TaskConnectionItem::updatePositions()
|
||||
{
|
||||
// calculatePositions();
|
||||
}
|
||||
|
||||
void TaskConnectionItem::calculatePositions()
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find source task item
|
||||
QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask());
|
||||
QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask());
|
||||
|
||||
if (!sourceTaskItem || !targetTaskItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find port items within tasks
|
||||
QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort());
|
||||
QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort());
|
||||
|
||||
if (!sourcePortItem || !targetPortItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate global positions
|
||||
QPointF sourceGlobal
|
||||
= sourcePortItem
|
||||
->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2);
|
||||
QPointF targetGlobal
|
||||
= targetPortItem
|
||||
->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2);
|
||||
|
||||
if (m_startPoint != sourceGlobal) {
|
||||
m_startPoint = sourceGlobal;
|
||||
emit startPointChanged();
|
||||
}
|
||||
|
||||
if (m_endPoint != targetGlobal) {
|
||||
m_endPoint = targetGlobal;
|
||||
emit endPointChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task)
|
||||
{
|
||||
for (const QVariant &item : m_taskItems) {
|
||||
QQuickItem *taskItem = qvariant_cast<QQuickItem *>(item);
|
||||
if (!taskItem)
|
||||
continue;
|
||||
|
||||
QVariant taskProp = taskItem->property("task");
|
||||
if (taskProp.isValid() && taskProp.value<BaseTask *>() == task) {
|
||||
return taskItem;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task)
|
||||
{
|
||||
// Проверяем objectName и task property
|
||||
if (item->objectName() == "TaskItem") {
|
||||
QVariant taskProp = item->property("task");
|
||||
if (taskProp.isValid()) {
|
||||
BaseTask *itemTask = taskProp.value<BaseTask *>();
|
||||
if (itemTask == task) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рекурсивно ищем в детях
|
||||
auto children = item->childItems();
|
||||
|
||||
for (QQuickItem *child : children) {
|
||||
if (QQuickItem *found = findTaskItemRecursive(child, task)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port)
|
||||
{
|
||||
std::function<QQuickItem *(QQuickItem *)> findPortRecursive =
|
||||
[&](QQuickItem *item) -> QQuickItem * {
|
||||
// Проверяем objectName и port property
|
||||
if (item->objectName() == "TaskPortItem") {
|
||||
QVariant portProp = item->property("port");
|
||||
if (portProp.isValid()) {
|
||||
TaskPort *itemPort = portProp.value<TaskPort *>();
|
||||
if (itemPort == port) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рекурсивно ищем в детях
|
||||
for (QQuickItem *child : item->childItems()) {
|
||||
if (QQuickItem *found = findPortRecursive(child)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
return findPortRecursive(taskItem);
|
||||
}
|
||||
|
||||
QVariantList TaskConnectionItem::taskItems() const
|
||||
{
|
||||
return m_taskItems;
|
||||
}
|
||||
|
||||
void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems)
|
||||
{
|
||||
if (m_taskItems == newTaskItems)
|
||||
return;
|
||||
m_taskItems = newTaskItems;
|
||||
emit taskItemsChanged();
|
||||
|
||||
calculatePositions();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
55
TaskFlow/Editor/TaskConnectionItem.hpp
Normal file
@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "TaskConnection.hpp"
|
||||
#include <QPointF>
|
||||
#include <QQuickItem>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskConnectionItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged)
|
||||
Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged)
|
||||
Q_PROPERTY(
|
||||
TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
||||
|
||||
public:
|
||||
TaskConnectionItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QPointF startPoint() const { return m_startPoint; }
|
||||
QPointF endPoint() const { return m_endPoint; }
|
||||
|
||||
TaskConnection *connection() const { return m_connection; }
|
||||
void setConnection(TaskConnection *connection);
|
||||
|
||||
Q_INVOKABLE void updatePositions();
|
||||
|
||||
QVariantList taskItems() const;
|
||||
void setTaskItems(const QVariantList &newTaskItems);
|
||||
|
||||
signals:
|
||||
void startPointChanged();
|
||||
void endPointChanged();
|
||||
void connectionChanged();
|
||||
|
||||
void taskItemsChanged();
|
||||
|
||||
private:
|
||||
void calculatePositions();
|
||||
QQuickItem *findTaskItem(BaseTask *task);
|
||||
QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task);
|
||||
QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port);
|
||||
|
||||
private:
|
||||
TaskConnection *m_connection = nullptr;
|
||||
QPointF m_startPoint;
|
||||
QPointF m_endPoint;
|
||||
QVariantList m_taskItems;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
29
TaskFlow/Editor/TaskConnectionsModel.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include "TaskConnectionsModel.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_flow(flow)
|
||||
{}
|
||||
|
||||
int TaskConnectionsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_flow->connections().size();
|
||||
}
|
||||
|
||||
QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == TaskConnectionsRoles::TaskConnectionsRole)
|
||||
return QVariant::fromValue(m_flow->connections().at(index.row()));
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TaskConnectionsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
25
TaskFlow/Editor/TaskConnectionsModel.hpp
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
|
||||
#include <Flow.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskConnectionsModel : public QAbstractListModel
|
||||
{
|
||||
public:
|
||||
enum TaskConnectionsRoles { TaskConnectionsRole = Qt::UserRole };
|
||||
|
||||
explicit TaskConnectionsModel(Flow *flow, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
Flow *m_flow;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
69
TaskFlow/Editor/TaskItem.cpp
Normal file
@ -0,0 +1,69 @@
|
||||
#include "TaskItem.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskItem::TaskItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setObjectName("TaskItem");
|
||||
}
|
||||
|
||||
QString TaskItem::taskId() const
|
||||
{
|
||||
return m_taskId;
|
||||
}
|
||||
|
||||
void TaskItem::setTaskId(const QString &newTaskId)
|
||||
{
|
||||
if (m_taskId == newTaskId)
|
||||
return;
|
||||
m_taskId = newTaskId;
|
||||
emit taskIdChanged();
|
||||
}
|
||||
|
||||
QString TaskItem::taskType() const
|
||||
{
|
||||
return m_task ? m_task->taskType() : QString();
|
||||
}
|
||||
|
||||
BaseTask *TaskItem::task() const
|
||||
{
|
||||
return m_task;
|
||||
}
|
||||
|
||||
void TaskItem::setTask(BaseTask *newTask)
|
||||
{
|
||||
if (m_task == newTask)
|
||||
return;
|
||||
|
||||
m_task = newTask;
|
||||
|
||||
if (m_task) {
|
||||
m_taskId = m_task->taskId();
|
||||
|
||||
// Обновляем модели портов
|
||||
m_inputPorts = new TaskPortModel(m_task->getInputPorts(), this);
|
||||
m_outputPorts = new TaskPortModel(m_task->getOutputPorts(), this);
|
||||
} else {
|
||||
m_inputPorts = nullptr;
|
||||
m_outputPorts = nullptr;
|
||||
}
|
||||
|
||||
emit taskChanged();
|
||||
emit inputPortsChanged();
|
||||
emit outputPortsChanged();
|
||||
emit taskIdChanged();
|
||||
emit taskTypeChanged();
|
||||
}
|
||||
|
||||
TaskPortModel *TaskItem::inputPorts() const
|
||||
{
|
||||
return m_inputPorts;
|
||||
}
|
||||
|
||||
TaskPortModel *TaskItem::outputPorts() const
|
||||
{
|
||||
return m_outputPorts;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
49
TaskFlow/Editor/TaskItem.hpp
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "TaskPortModel.hpp"
|
||||
#include <BaseTask.hpp>
|
||||
#include <TaskPort.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString taskId READ taskId WRITE setTaskId NOTIFY taskIdChanged)
|
||||
Q_PROPERTY(QString taskType READ taskType NOTIFY taskTypeChanged)
|
||||
Q_PROPERTY(BaseTask *task READ task WRITE setTask NOTIFY taskChanged)
|
||||
Q_PROPERTY(TaskPortModel *inputPorts READ inputPorts NOTIFY inputPortsChanged)
|
||||
Q_PROPERTY(TaskPortModel *outputPorts READ outputPorts NOTIFY outputPortsChanged)
|
||||
|
||||
public:
|
||||
TaskItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QString taskId() const;
|
||||
void setTaskId(const QString &newTaskId);
|
||||
QString taskType() const;
|
||||
|
||||
BaseTask *task() const;
|
||||
void setTask(BaseTask *newTask);
|
||||
|
||||
TaskPortModel *inputPorts() const;
|
||||
TaskPortModel *outputPorts() const;
|
||||
|
||||
signals:
|
||||
void taskIdChanged();
|
||||
void taskTypeChanged();
|
||||
void taskChanged();
|
||||
void inputPortsChanged();
|
||||
void outputPortsChanged();
|
||||
|
||||
private:
|
||||
QString m_taskId;
|
||||
BaseTask *m_task = nullptr;
|
||||
TaskPortModel *m_inputPorts = nullptr;
|
||||
TaskPortModel *m_outputPorts = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
40
TaskFlow/Editor/TaskModel.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
#include "TaskModel.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskModel::TaskModel(Flow *flow, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_flow(flow)
|
||||
{}
|
||||
|
||||
int TaskModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_flow->tasks().size();
|
||||
}
|
||||
|
||||
QVariant TaskModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || !m_flow || index.row() >= m_flow->tasks().size())
|
||||
return QVariant();
|
||||
|
||||
const auto &task = m_flow->tasks().values();
|
||||
|
||||
switch (role) {
|
||||
case TaskRoles::TaskIdRole:
|
||||
return task.at(index.row())->taskId();
|
||||
case TaskRoles::TaskDataRole:
|
||||
return QVariant::fromValue(task.at(index.row()));
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TaskModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[TaskRoles::TaskIdRole] = "taskId";
|
||||
roles[TaskRoles::TaskDataRole] = "taskData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
25
TaskFlow/Editor/TaskModel.hpp
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include <Flow.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum TaskRoles { TaskIdRole = Qt::UserRole, TaskDataRole };
|
||||
|
||||
TaskModel(Flow *flow, QObject *parent);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
Flow *m_flow;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
29
TaskFlow/Editor/TaskPortItem.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include "TaskPortItem.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskPortItem::TaskPortItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setObjectName("TaskPortItem");
|
||||
}
|
||||
|
||||
TaskPort *TaskPortItem::port() const
|
||||
{
|
||||
return m_port;
|
||||
}
|
||||
|
||||
void TaskPortItem::setPort(TaskPort *newPort)
|
||||
{
|
||||
if (m_port == newPort)
|
||||
return;
|
||||
m_port = newPort;
|
||||
emit portChanged();
|
||||
}
|
||||
|
||||
QString TaskPortItem::name() const
|
||||
{
|
||||
return m_port->name();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
31
TaskFlow/Editor/TaskPortItem.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <TaskPort.hpp>
|
||||
#include <QQuickItem>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskPortItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(TaskPort *port READ port WRITE setPort NOTIFY portChanged)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
|
||||
public:
|
||||
TaskPortItem(QQuickItem *parent = nullptr);
|
||||
|
||||
TaskPort *port() const;
|
||||
void setPort(TaskPort *newPort);
|
||||
|
||||
QString name() const;
|
||||
|
||||
signals:
|
||||
void portChanged();
|
||||
|
||||
private:
|
||||
TaskPort *m_port = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
39
TaskFlow/Editor/TaskPortModel.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#include "TaskPortModel.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskPortModel::TaskPortModel(const QList<TaskPort *> &ports, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_ports(ports)
|
||||
{}
|
||||
|
||||
int TaskPortModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_ports.size();
|
||||
}
|
||||
|
||||
QVariant TaskPortModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_ports.size())
|
||||
return QVariant();
|
||||
|
||||
switch (role) {
|
||||
case TaskPortRoles::TaskPortNameRole:
|
||||
return m_ports.at(index.row())->name();
|
||||
case TaskPortRoles::TaskPortDataRole:
|
||||
return QVariant::fromValue(m_ports.at(index.row()));
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TaskPortModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[TaskPortRoles::TaskPortNameRole] = "taskPortName";
|
||||
roles[TaskPortRoles::TaskPortDataRole] = "taskPortData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
25
TaskFlow/Editor/TaskPortModel.hpp
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
#include <BaseTask.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskPortModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole };
|
||||
|
||||
TaskPortModel(const QList<TaskPort *> &ports, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private:
|
||||
QList<TaskPort *> m_ports;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
134
TaskFlow/Editor/qml/Flow.qml
Normal file
@ -0,0 +1,134 @@
|
||||
import QtQuick
|
||||
import TaskFlow.Editor
|
||||
|
||||
FlowItem {
|
||||
id: root
|
||||
|
||||
Repeater {
|
||||
id: tasks
|
||||
|
||||
model: root.taskModel
|
||||
delegate: Task {
|
||||
// task: taskData
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: connections
|
||||
|
||||
model: root.taskModel
|
||||
delegate: TaskConnection {
|
||||
// task: taskData
|
||||
}
|
||||
}
|
||||
|
||||
// property var qtaskItems: []
|
||||
|
||||
// // Flow container background
|
||||
// Rectangle {
|
||||
// anchors.fill: parent
|
||||
// color: palette.alternateBase
|
||||
// border.color: palette.mid
|
||||
// border.width: 2
|
||||
// radius: 8
|
||||
|
||||
// // Flow header
|
||||
// Rectangle {
|
||||
// id: flowHeader
|
||||
// anchors.top: parent.top
|
||||
// anchors.left: parent.left
|
||||
// anchors.right: parent.right
|
||||
// height: 40
|
||||
// color: palette.button
|
||||
// radius: 6
|
||||
|
||||
// Rectangle {
|
||||
// anchors.bottom: parent.bottom
|
||||
// anchors.left: parent.left
|
||||
// anchors.right: parent.right
|
||||
// height: parent.radius
|
||||
// color: parent.color
|
||||
// }
|
||||
|
||||
// Text {
|
||||
// anchors.centerIn: parent
|
||||
// text: root.flowId
|
||||
// color: palette.buttonText
|
||||
// font.pixelSize: 14
|
||||
// font.bold: true
|
||||
// }
|
||||
// }
|
||||
|
||||
// // // Tasks container
|
||||
// // Row {
|
||||
// // id: tasksRow
|
||||
// // anchors.top: flowHeader.bottom
|
||||
// // anchors.left: parent.left
|
||||
// // anchors.margins: 25
|
||||
// // anchors.topMargin: 25
|
||||
// // objectName: "FlowTaskRow"
|
||||
|
||||
// // spacing: 40
|
||||
|
||||
// // Repeater {
|
||||
// // model: root.taskModel
|
||||
|
||||
// // delegate: Task {
|
||||
// // task: taskData
|
||||
// // }
|
||||
|
||||
// // onItemAdded: function(index, item){
|
||||
// // console.log("task added", index, item)
|
||||
// // qtaskItems.push(item)
|
||||
// // root.insertTaskItem(index, item)
|
||||
// // }
|
||||
|
||||
// // onItemRemoved: function(index, item){
|
||||
// // console.log("task added", index, item)
|
||||
// // var idx = qtaskItems.indexOf(item)
|
||||
// // if (idx !== -1) qtaskItems.splice(idx, 1)
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // Repeater {
|
||||
// // model: root.connectionsModel
|
||||
|
||||
// // delegate: TaskConnection {
|
||||
// // connection: connectionData
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
// // Flow info tooltip
|
||||
// Rectangle {
|
||||
// id: infoTooltip
|
||||
// anchors.top: parent.bottom
|
||||
// anchors.left: parent.left
|
||||
// anchors.topMargin: 5
|
||||
// width: infoText.width + 20
|
||||
// height: infoText.height + 10
|
||||
// color: palette.base
|
||||
// border.color: palette.shadow
|
||||
// border.width: 1
|
||||
// radius: 4
|
||||
// visible: false
|
||||
|
||||
// Text {
|
||||
// id: infoText
|
||||
// anchors.centerIn: parent
|
||||
// text: "Tasks: " + (root.taskModel ? root.taskModel.rowCount() : 0)
|
||||
// color: palette.text
|
||||
// font.pixelSize: 10
|
||||
// }
|
||||
// }
|
||||
|
||||
// MouseArea {
|
||||
// anchors.fill: parent
|
||||
// hoverEnabled: true
|
||||
|
||||
// onEntered: infoTooltip.visible = true
|
||||
// onExited: infoTooltip.visible = false
|
||||
// }
|
||||
|
||||
}
|
||||
140
TaskFlow/Editor/qml/FlowEditorView.qml
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import TaskFlow.Editor
|
||||
|
||||
FlowEditor {
|
||||
id: root
|
||||
|
||||
width: 1200
|
||||
height: 800
|
||||
|
||||
property SystemPalette sysPalette: SystemPalette {
|
||||
colorGroup: SystemPalette.Active
|
||||
}
|
||||
palette {
|
||||
window: sysPalette.window
|
||||
windowText: sysPalette.windowText
|
||||
base: sysPalette.base
|
||||
alternateBase: sysPalette.alternateBase
|
||||
text: sysPalette.text
|
||||
button: sysPalette.button
|
||||
buttonText: sysPalette.buttonText
|
||||
highlight: sysPalette.highlight
|
||||
highlightedText: sysPalette.highlightedText
|
||||
light: sysPalette.light
|
||||
mid: sysPalette.mid
|
||||
dark: sysPalette.dark
|
||||
shadow: sysPalette.shadow
|
||||
brightText: sysPalette.brightText
|
||||
}
|
||||
|
||||
// Background with grid pattern
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: palette.window
|
||||
|
||||
// Grid pattern using C++ implementation
|
||||
GridBackground {
|
||||
anchors.fill: parent
|
||||
gridSize: 20
|
||||
gridColor: palette.mid
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// Header panel
|
||||
Rectangle {
|
||||
id: headerPanel
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 60
|
||||
color: palette.base
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 20
|
||||
spacing: 20
|
||||
|
||||
Text {
|
||||
text: "Flow Editor"
|
||||
color: palette.windowText
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 2
|
||||
height: 30
|
||||
color: palette.mid
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Flow:"
|
||||
color: palette.text
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: flowComboBox
|
||||
|
||||
model: root.flowsModel
|
||||
textRole: "flowId"
|
||||
currentIndex: root.currentFlowIndex
|
||||
|
||||
onActivated: {
|
||||
root.currentFlowIndex = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Available Tasks: " + root.availableTaskTypes.join(", ")
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main flow area
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.top: headerPanel.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
contentWidth: flow.width
|
||||
contentHeight: flow.height
|
||||
|
||||
Flow {
|
||||
id: flow
|
||||
|
||||
// flow: root.currentFlow
|
||||
|
||||
width: Math.max(root.width, 0)
|
||||
height: Math.min(root.height, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
210
TaskFlow/Editor/qml/Task.qml
Normal file
@ -0,0 +1,210 @@
|
||||
import QtQuick
|
||||
import TaskFlow.Editor
|
||||
|
||||
TaskItem{
|
||||
id: root
|
||||
|
||||
width: 280
|
||||
height: Math.max(200, contentColumn.height + 40)
|
||||
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
|
||||
target: root
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
root.z = 1000; // Поднять над остальными
|
||||
} else {
|
||||
root.z = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task node background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: palette.window
|
||||
border.color: palette.shadow
|
||||
border.width: 1
|
||||
radius: 6
|
||||
|
||||
// Task header
|
||||
Rectangle {
|
||||
id: taskHeader
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 40
|
||||
color: palette.button
|
||||
radius: 6
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.radius
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
// text: root.taskType
|
||||
color: palette.buttonText
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// Task content
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.top: taskHeader.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
// Task ID
|
||||
Text {
|
||||
text: "ID: " + root.taskId
|
||||
color: palette.text
|
||||
font.pixelSize: 11
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Parameters section
|
||||
Item {
|
||||
width: parent.width
|
||||
height: paramColumn.height
|
||||
// visible: root.parameters && root.parameters.rowCount() > 0
|
||||
|
||||
Column {
|
||||
id: paramColumn
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: "Parameters:"
|
||||
color: palette.text
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.parameters
|
||||
delegate: Rectangle {
|
||||
width: parent.width
|
||||
height: 24
|
||||
color: palette.base
|
||||
radius: 4
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 8
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: paramKey + ":"
|
||||
color: palette.text
|
||||
font.pixelSize: 9
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: paramValue
|
||||
color: palette.windowText
|
||||
font.pixelSize: 9
|
||||
width: Math.min(150, implicitWidth)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input ports section (left side)
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: -8
|
||||
spacing: 6
|
||||
// visible: root.inputPorts && root.inputPorts.rowCount() > 0
|
||||
|
||||
// Input label
|
||||
Text {
|
||||
text: "IN"
|
||||
color: palette.highlight
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: -20
|
||||
}
|
||||
|
||||
// Repeater {
|
||||
// model: root.inputPorts
|
||||
// delegate: Row {
|
||||
// spacing: 6
|
||||
|
||||
// Text {
|
||||
// text: taskPortName
|
||||
// color: palette.text
|
||||
// font.pixelSize: 9
|
||||
// anchors.verticalCenter: parent.verticalCenter
|
||||
// horizontalAlignment: Text.AlignRight
|
||||
// width: 60
|
||||
// elide: Text.ElideLeft
|
||||
// }
|
||||
|
||||
// TaskPort {
|
||||
// port: taskPortData
|
||||
// isInput: true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Output ports section (right side)
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: -10
|
||||
spacing: 8
|
||||
// visible: root.outputPorts && root.outputPorts.rowCount() > 0
|
||||
|
||||
// Output label
|
||||
Text {
|
||||
text: "OUT"
|
||||
color: palette.highlight
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: -24
|
||||
}
|
||||
|
||||
// Repeater {
|
||||
// model: root.outputPorts
|
||||
// delegate: Row {
|
||||
// spacing: 6
|
||||
|
||||
// TaskPort {
|
||||
// port: taskPortData
|
||||
// isInput: false
|
||||
// }
|
||||
|
||||
// Text {
|
||||
// text: taskPortName
|
||||
// color: palette.text
|
||||
// font.pixelSize: 9
|
||||
// anchors.verticalCenter: parent.verticalCenter
|
||||
// width: 60
|
||||
// elide: Text.ElideRight
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
68
TaskFlow/Editor/qml/TaskConnection.qml
Normal file
@ -0,0 +1,68 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import TaskFlow.Editor
|
||||
|
||||
TaskConnectionItem {
|
||||
id: root
|
||||
|
||||
property color connectionColor: "red"
|
||||
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: 10
|
||||
radius: width / 2
|
||||
color: "blue"
|
||||
}
|
||||
|
||||
// width: Math.abs(endPoint.x - startPoint.x) + 40
|
||||
// height: Math.abs(endPoint.y - startPoint.y) + 40
|
||||
// x: Math.min(startPoint.x, endPoint.x) - 20
|
||||
// y: Math.min(startPoint.y, endPoint.y) - 20
|
||||
|
||||
// Shape {
|
||||
// anchors.fill: parent
|
||||
|
||||
// ShapePath {
|
||||
// strokeWidth: 2
|
||||
// strokeColor: connectionColor
|
||||
// fillColor: "transparent"
|
||||
|
||||
// property point localStart: Qt.point(
|
||||
// root.startPoint.x - root.x,
|
||||
// root.startPoint.y - root.y
|
||||
// )
|
||||
// property point localEnd: Qt.point(
|
||||
// root.endPoint.x - root.x,
|
||||
// root.endPoint.y - root.y
|
||||
// )
|
||||
|
||||
// // Bezier curve
|
||||
// property real controlOffset: Math.max(50, Math.abs(localEnd.x - localStart.x) * 0.4)
|
||||
|
||||
// startX: localStart.x
|
||||
// startY: localStart.y
|
||||
|
||||
// PathCubic {
|
||||
// x: parent.localEnd.x
|
||||
// y: parent.localEnd.y
|
||||
// control1X: parent.localStart.x + parent.controlOffset
|
||||
// control1Y: parent.localStart.y
|
||||
// control2X: parent.localEnd.x - parent.controlOffset
|
||||
// control2Y: parent.localEnd.y
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Arrow head
|
||||
// Rectangle {
|
||||
// width: 8
|
||||
// height: 8
|
||||
// color: connectionColor
|
||||
// rotation: 45
|
||||
// x: root.endPoint.x - root.x - 4
|
||||
// y: root.endPoint.y - root.y - 4
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Update positions when tasks might have moved
|
||||
// Component.onCompleted: updatePositions()
|
||||
}
|
||||
6
TaskFlow/Editor/qml/TaskParameter.qml
Normal file
@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
import TaskFlow.Editor
|
||||
|
||||
Item {
|
||||
|
||||
}
|
||||
63
TaskFlow/Editor/qml/TaskPort.qml
Normal file
@ -0,0 +1,63 @@
|
||||
import QtQuick
|
||||
import TaskFlow.Editor
|
||||
|
||||
TaskPortItem {
|
||||
id: root
|
||||
|
||||
property bool isInput: true
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
|
||||
// Port circle
|
||||
Rectangle {
|
||||
id: portCircle
|
||||
anchors.centerIn: parent
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
color: getPortColor()
|
||||
border.color: palette.windowText
|
||||
border.width: 1
|
||||
|
||||
// Inner circle for connected state simulation
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: root.port ? palette.windowText : "transparent"
|
||||
visible: root.port !== null
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
portCircle.scale = 1.3
|
||||
portCircle.border.width = 2
|
||||
}
|
||||
|
||||
onExited: {
|
||||
portCircle.scale = 1.0
|
||||
portCircle.border.width = 1
|
||||
}
|
||||
}
|
||||
|
||||
function getPortColor() {
|
||||
if (!root.port) return palette.mid
|
||||
|
||||
// Different colors for input/output using system palette
|
||||
if (root.isInput) {
|
||||
return palette.highlight // System highlight color for inputs
|
||||
} else {
|
||||
return Qt.lighter(palette.highlight, 1.3) // Lighter highlight for outputs
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
117
TaskFlow/core/BaseTask.cpp
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BaseTask.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
#include <QUuid>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
BaseTask::BaseTask(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_taskId("unknown" + QUuid::createUuid().toString())
|
||||
{}
|
||||
|
||||
BaseTask::~BaseTask()
|
||||
{
|
||||
qDeleteAll(m_inputs);
|
||||
qDeleteAll(m_outputs);
|
||||
}
|
||||
|
||||
QString BaseTask::taskId() const
|
||||
{
|
||||
return m_taskId;
|
||||
}
|
||||
|
||||
void BaseTask::setTaskId(const QString &taskId)
|
||||
{
|
||||
m_taskId = taskId;
|
||||
}
|
||||
|
||||
QString BaseTask::taskType() const
|
||||
{
|
||||
return QString(metaObject()->className()).split("::").last();
|
||||
}
|
||||
|
||||
void BaseTask::addInputPort(const QString &name)
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
m_inputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
|
||||
}
|
||||
|
||||
void BaseTask::addOutputPort(const QString &name)
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
m_outputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
|
||||
}
|
||||
|
||||
TaskPort *BaseTask::inputPort(const QString &name) const
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
|
||||
auto it = std::find_if(m_inputs.begin(), m_inputs.end(), [&name](const TaskPort *port) {
|
||||
return port->name() == name;
|
||||
});
|
||||
|
||||
return (it != m_inputs.end()) ? *it : nullptr;
|
||||
}
|
||||
|
||||
TaskPort *BaseTask::outputPort(const QString &name) const
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
|
||||
auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&name](const TaskPort *port) {
|
||||
return port->name() == name;
|
||||
});
|
||||
|
||||
return (it != m_outputs.end()) ? *it : nullptr;
|
||||
}
|
||||
|
||||
QList<TaskPort *> BaseTask::getInputPorts() const
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
return m_inputs;
|
||||
}
|
||||
|
||||
QList<TaskPort *> BaseTask::getOutputPorts() const
|
||||
{
|
||||
QMutexLocker locker(&m_tasksMutex);
|
||||
return m_outputs;
|
||||
}
|
||||
|
||||
QFuture<TaskState> BaseTask::executeAsync()
|
||||
{
|
||||
return QtConcurrent::task([this]() -> TaskState { return execute(); }).spawn();
|
||||
}
|
||||
|
||||
QString BaseTask::taskStateAsString(TaskState state)
|
||||
{
|
||||
switch (state) {
|
||||
case TaskState::Success:
|
||||
return "Success";
|
||||
case TaskState::Failed:
|
||||
return "Failed";
|
||||
case TaskState::Cancelled:
|
||||
return "Cancelled";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
68
TaskFlow/core/BaseTask.hpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFuture>
|
||||
#include <QMetaType>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskPort;
|
||||
|
||||
enum class TaskState { Success, Failed, Cancelled };
|
||||
|
||||
class BaseTask : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BaseTask(QObject *parent = nullptr);
|
||||
virtual ~BaseTask();
|
||||
|
||||
QString taskId() const;
|
||||
void setTaskId(const QString &taskId);
|
||||
QString taskType() const;
|
||||
|
||||
void addInputPort(const QString &name);
|
||||
void addOutputPort(const QString &name);
|
||||
|
||||
TaskPort *inputPort(const QString &name) const;
|
||||
TaskPort *outputPort(const QString &name) const;
|
||||
|
||||
QList<TaskPort *> getInputPorts() const;
|
||||
QList<TaskPort *> getOutputPorts() const;
|
||||
|
||||
virtual TaskState execute() = 0;
|
||||
|
||||
static QString taskStateAsString(TaskState state);
|
||||
|
||||
protected:
|
||||
QFuture<TaskState> executeAsync();
|
||||
|
||||
private:
|
||||
QString m_taskId;
|
||||
QList<TaskPort *> m_inputs;
|
||||
QList<TaskPort *> m_outputs;
|
||||
mutable QMutex m_tasksMutex;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
22
TaskFlow/core/CMakeLists.txt
Normal file
@ -0,0 +1,22 @@
|
||||
qt_add_library(TaskFlowCore STATIC
|
||||
BaseTask.hpp BaseTask.cpp
|
||||
TaskConnection.hpp TaskConnection.cpp
|
||||
Flow.hpp Flow.cpp
|
||||
TaskPort.hpp TaskPort.cpp
|
||||
TaskRegistry.hpp TaskRegistry.cpp
|
||||
FlowManager.hpp FlowManager.cpp
|
||||
FlowRegistry.hpp FlowRegistry.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(TaskFlowCore
|
||||
PUBLIC
|
||||
Qt::Core
|
||||
Qt::Concurrent
|
||||
PRIVATE
|
||||
QodeAssistLogger
|
||||
)
|
||||
|
||||
target_include_directories(TaskFlowCore
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
355
TaskFlow/core/Flow.cpp
Normal file
@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Flow.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
#include <QUuid>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
Flow::Flow(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_flowId("flow_" + QUuid::createUuid().toString())
|
||||
{}
|
||||
|
||||
Flow::~Flow()
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
qDeleteAll(m_connections);
|
||||
qDeleteAll(m_tasks);
|
||||
}
|
||||
|
||||
QString Flow::flowId() const
|
||||
{
|
||||
return m_flowId;
|
||||
}
|
||||
|
||||
void Flow::setFlowId(const QString &flowId)
|
||||
{
|
||||
if (m_flowId != flowId) {
|
||||
m_flowId = flowId;
|
||||
}
|
||||
}
|
||||
|
||||
void Flow::addTask(BaseTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
QString taskId = task->taskId();
|
||||
if (m_tasks.contains(taskId)) {
|
||||
qWarning() << "Flow::addTask - Task with ID" << taskId << "already exists";
|
||||
return;
|
||||
}
|
||||
|
||||
m_tasks.insert(taskId, task);
|
||||
task->setParent(this);
|
||||
|
||||
emit taskAdded(taskId);
|
||||
}
|
||||
|
||||
void Flow::removeTask(const QString &taskId)
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
BaseTask *task = m_tasks.value(taskId);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = m_connections.begin();
|
||||
while (it != m_connections.end()) {
|
||||
TaskConnection *connection = *it;
|
||||
if (connection->sourceTask() == task || connection->targetTask() == task) {
|
||||
it = m_connections.erase(it);
|
||||
emit connectionRemoved(connection);
|
||||
delete connection;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
m_tasks.remove(taskId);
|
||||
emit taskRemoved(taskId);
|
||||
delete task;
|
||||
}
|
||||
|
||||
void Flow::removeTask(BaseTask *task)
|
||||
{
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
removeTask(task->taskId());
|
||||
}
|
||||
|
||||
BaseTask *Flow::getTask(const QString &taskId) const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
return m_tasks.value(taskId);
|
||||
}
|
||||
|
||||
bool Flow::hasTask(const QString &taskId) const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
return m_tasks.contains(taskId);
|
||||
}
|
||||
|
||||
QHash<QString, BaseTask *> Flow::tasks() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
return m_tasks;
|
||||
}
|
||||
|
||||
TaskConnection *Flow::addConnection(TaskPort *sourcePort, TaskPort *targetPort)
|
||||
{
|
||||
if (!sourcePort || !targetPort) {
|
||||
qWarning() << "Flow::addConnection - Invalid ports";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Verify ports belong to tasks in this flow
|
||||
BaseTask *sourceTask = qobject_cast<BaseTask *>(sourcePort->parent());
|
||||
BaseTask *targetTask = qobject_cast<BaseTask *>(targetPort->parent());
|
||||
|
||||
if (!sourceTask || !targetTask) {
|
||||
qWarning() << "Flow::addConnection - Ports don't belong to valid tasks";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
if (!m_tasks.contains(sourceTask->taskId()) || !m_tasks.contains(targetTask->taskId())) {
|
||||
qWarning() << "Flow::addConnection - Tasks not in this flow";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (TaskConnection *existingConnection : m_connections) {
|
||||
if (existingConnection->sourcePort() == sourcePort
|
||||
&& existingConnection->targetPort() == targetPort) {
|
||||
qWarning() << "Flow::addConnection - Connection already exists";
|
||||
return existingConnection;
|
||||
}
|
||||
}
|
||||
|
||||
TaskConnection *connection = new TaskConnection(sourcePort, targetPort, this);
|
||||
m_connections.append(connection);
|
||||
|
||||
emit connectionAdded(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
void Flow::removeConnection(TaskConnection *connection)
|
||||
{
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
if (m_connections.removeOne(connection)) {
|
||||
emit connectionRemoved(connection);
|
||||
delete connection;
|
||||
}
|
||||
}
|
||||
|
||||
QList<TaskConnection *> Flow::connections() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
return m_connections;
|
||||
}
|
||||
|
||||
QFuture<FlowState> Flow::executeAsync()
|
||||
{
|
||||
return QtConcurrent::run([this]() { return execute(); });
|
||||
}
|
||||
|
||||
FlowState Flow::execute()
|
||||
{
|
||||
emit executionStarted();
|
||||
|
||||
if (!isValid()) {
|
||||
emit executionFinished(FlowState::Failed);
|
||||
return FlowState::Failed;
|
||||
}
|
||||
|
||||
if (hasCircularDependencies()) {
|
||||
qWarning() << "Flow::execute - Circular dependencies detected";
|
||||
emit executionFinished(FlowState::Failed);
|
||||
return FlowState::Failed;
|
||||
}
|
||||
|
||||
QList<BaseTask *> executionOrder = getExecutionOrder();
|
||||
|
||||
for (BaseTask *task : executionOrder) {
|
||||
TaskState taskResult = task->execute();
|
||||
|
||||
if (taskResult == TaskState::Failed) {
|
||||
qWarning() << "Flow::execute - Task" << task->taskId() << "failed";
|
||||
emit executionFinished(FlowState::Failed);
|
||||
return FlowState::Failed;
|
||||
}
|
||||
|
||||
if (taskResult == TaskState::Cancelled) {
|
||||
qWarning() << "Flow::execute - Task" << task->taskId() << "cancelled";
|
||||
emit executionFinished(FlowState::Cancelled);
|
||||
return FlowState::Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
emit executionFinished(FlowState::Success);
|
||||
return FlowState::Success;
|
||||
}
|
||||
|
||||
bool Flow::isValid() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
// Check all connections are valid
|
||||
for (TaskConnection *connection : m_connections) {
|
||||
if (!connection->isValid()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Flow::hasCircularDependencies() const
|
||||
{
|
||||
return detectCircularDependencies();
|
||||
}
|
||||
|
||||
QString Flow::flowStateAsString(FlowState state)
|
||||
{
|
||||
switch (state) {
|
||||
case FlowState::Success:
|
||||
return "Success";
|
||||
case FlowState::Failed:
|
||||
return "Failed";
|
||||
case FlowState::Cancelled:
|
||||
return "Cancelled";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
QStringList Flow::getTaskIds() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
return m_tasks.keys();
|
||||
}
|
||||
|
||||
QList<BaseTask *> Flow::getExecutionOrder() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
QList<BaseTask *> result;
|
||||
QSet<BaseTask *> visited;
|
||||
QList<BaseTask *> allTasks = m_tasks.values();
|
||||
|
||||
std::function<void(BaseTask *)> visit = [&](BaseTask *task) {
|
||||
if (visited.contains(task)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.insert(task);
|
||||
|
||||
QList<BaseTask *> dependencies = getTaskDependencies(task);
|
||||
for (BaseTask *dependency : dependencies) {
|
||||
visit(dependency);
|
||||
}
|
||||
|
||||
result.append(task);
|
||||
};
|
||||
|
||||
for (BaseTask *task : allTasks) {
|
||||
visit(task);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Flow::detectCircularDependencies() const
|
||||
{
|
||||
QMutexLocker locker(&m_flowMutex);
|
||||
|
||||
QSet<BaseTask *> visited;
|
||||
QSet<BaseTask *> recursionStack;
|
||||
bool hasCycle = false;
|
||||
|
||||
for (BaseTask *task : m_tasks.values()) {
|
||||
if (!visited.contains(task)) {
|
||||
visitTask(task, visited, recursionStack, hasCycle);
|
||||
if (hasCycle) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Flow::visitTask(
|
||||
BaseTask *task, QSet<BaseTask *> &visited, QSet<BaseTask *> &recursionStack, bool &hasCycle) const
|
||||
{
|
||||
if (hasCycle) {
|
||||
return;
|
||||
}
|
||||
|
||||
visited.insert(task);
|
||||
recursionStack.insert(task);
|
||||
|
||||
for (TaskConnection *connection : m_connections) {
|
||||
if (connection->sourceTask() == task) {
|
||||
BaseTask *dependentTask = connection->targetTask();
|
||||
|
||||
if (recursionStack.contains(dependentTask)) {
|
||||
hasCycle = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visited.contains(dependentTask)) {
|
||||
visitTask(dependentTask, visited, recursionStack, hasCycle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.remove(task);
|
||||
}
|
||||
|
||||
QList<BaseTask *> Flow::getTaskDependencies(BaseTask *task) const
|
||||
{
|
||||
QList<BaseTask *> dependencies;
|
||||
|
||||
for (TaskConnection *connection : m_connections) {
|
||||
if (connection->targetTask() == task) {
|
||||
BaseTask *dependencyTask = connection->sourceTask();
|
||||
if (!dependencies.contains(dependencyTask)) {
|
||||
dependencies.append(dependencyTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
95
TaskFlow/core/Flow.hpp
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QFuture>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QMetaType>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
|
||||
#include "BaseTask.hpp"
|
||||
#include "TaskConnection.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
enum class FlowState { Success, Failed, Cancelled };
|
||||
|
||||
class Flow : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Flow(QObject *parent = nullptr);
|
||||
~Flow() override;
|
||||
|
||||
QString flowId() const;
|
||||
void setFlowId(const QString &flowId);
|
||||
|
||||
void addTask(BaseTask *task);
|
||||
void removeTask(const QString &taskId);
|
||||
void removeTask(BaseTask *task);
|
||||
|
||||
BaseTask *getTask(const QString &taskId) const;
|
||||
bool hasTask(const QString &taskId) const;
|
||||
QHash<QString, BaseTask *> tasks() const;
|
||||
|
||||
TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort);
|
||||
void removeConnection(TaskConnection *connection);
|
||||
QList<TaskConnection *> connections() const;
|
||||
|
||||
QFuture<FlowState> executeAsync();
|
||||
virtual FlowState execute();
|
||||
|
||||
bool isValid() const;
|
||||
bool hasCircularDependencies() const;
|
||||
|
||||
static QString flowStateAsString(FlowState state);
|
||||
QStringList getTaskIds() const;
|
||||
|
||||
signals:
|
||||
void taskAdded(const QString &taskId);
|
||||
void taskRemoved(const QString &taskId);
|
||||
void connectionAdded(QodeAssist::TaskFlow::TaskConnection *connection);
|
||||
void connectionRemoved(QodeAssist::TaskFlow::TaskConnection *connection);
|
||||
void executionStarted();
|
||||
void executionFinished(FlowState result);
|
||||
|
||||
private:
|
||||
QString m_flowId;
|
||||
QHash<QString, BaseTask *> m_tasks;
|
||||
QList<TaskConnection *> m_connections;
|
||||
mutable QMutex m_flowMutex;
|
||||
|
||||
QList<BaseTask *> getExecutionOrder() const;
|
||||
bool detectCircularDependencies() const;
|
||||
void visitTask(
|
||||
BaseTask *task,
|
||||
QSet<BaseTask *> &visited,
|
||||
QSet<BaseTask *> &recursionStack,
|
||||
bool &hasCycle) const;
|
||||
QList<BaseTask *> getTaskDependencies(BaseTask *task) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
|
||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *)
|
||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState)
|
||||
112
TaskFlow/core/FlowManager.cpp
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "FlowManager.hpp"
|
||||
|
||||
#include <Logger.hpp>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "FlowRegistry.hpp"
|
||||
#include "TaskRegistry.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowManager::FlowManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_taskRegistry(new TaskRegistry(this))
|
||||
, m_flowRegistry(new FlowRegistry(this))
|
||||
{
|
||||
LOG_MESSAGE("FlowManager created");
|
||||
}
|
||||
|
||||
FlowManager::~FlowManager()
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
// Flow *FlowManager::createFlow(const QString &flowId)
|
||||
// {
|
||||
// Flow *flow = new Flow(flowId, m_taskRegistry, this);
|
||||
// if (!m_flows.contains(flow->flowId())) {
|
||||
// m_flows.insert(flowId, flow);
|
||||
// } else {
|
||||
// LOG_MESSAGE(
|
||||
// QString("FlowManager::createFlow - flow with id %1 already exists").arg(flow->flowId()));
|
||||
// }
|
||||
|
||||
// return flow;
|
||||
// }
|
||||
|
||||
void FlowManager::addFlow(Flow *flow)
|
||||
{
|
||||
qDebug() << "FlowManager::addFlow" << flow->flowId();
|
||||
if (!m_flows.contains(flow->flowId())) {
|
||||
m_flows.insert(flow->flowId(), flow);
|
||||
flow->setParent(this);
|
||||
emit flowAdded(flow->flowId());
|
||||
} else {
|
||||
LOG_MESSAGE(
|
||||
QString("FlowManager::addFlow - flow with id %1 already exists").arg(flow->flowId()));
|
||||
}
|
||||
}
|
||||
|
||||
void FlowManager::clear()
|
||||
{
|
||||
LOG_MESSAGE(QString("FlowManager::clear - removing %1 flows").arg(m_flows.size()));
|
||||
|
||||
qDeleteAll(m_flows);
|
||||
m_flows.clear();
|
||||
}
|
||||
|
||||
QStringList FlowManager::getAvailableTasksTypes()
|
||||
{
|
||||
return m_taskRegistry->getAvailableTypes();
|
||||
}
|
||||
|
||||
QStringList FlowManager::getAvailableFlows()
|
||||
{
|
||||
return m_flowRegistry->getAvailableTypes();
|
||||
}
|
||||
|
||||
QHash<QString, Flow *> FlowManager::flows() const
|
||||
{
|
||||
return m_flows;
|
||||
}
|
||||
|
||||
TaskRegistry *FlowManager::taskRegistry() const
|
||||
{
|
||||
return m_taskRegistry;
|
||||
}
|
||||
|
||||
FlowRegistry *FlowManager::flowRegistry() const
|
||||
{
|
||||
return m_flowRegistry;
|
||||
}
|
||||
|
||||
Flow *FlowManager::getFlow(const QString &flowId) const
|
||||
{
|
||||
// if (flowId.isEmpty()) {
|
||||
// return m_flows.begin().value();
|
||||
// }
|
||||
// return m_flows.value(flowId, nullptr);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
69
TaskFlow/core/FlowManager.hpp
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "Flow.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskRegistry;
|
||||
class FlowRegistry;
|
||||
|
||||
class FlowManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FlowManager(QObject *parent = nullptr);
|
||||
~FlowManager() override;
|
||||
|
||||
// Flow *createFlow(const QString &flowId);
|
||||
void addFlow(Flow *flow);
|
||||
|
||||
void clear();
|
||||
|
||||
QStringList getAvailableTasksTypes();
|
||||
QStringList getAvailableFlows();
|
||||
|
||||
QHash<QString, Flow *> flows() const;
|
||||
|
||||
TaskRegistry *taskRegistry() const;
|
||||
FlowRegistry *flowRegistry() const;
|
||||
|
||||
Flow *getFlow(const QString &flowId = {}) const;
|
||||
|
||||
signals:
|
||||
void flowAdded(const QString &flowId);
|
||||
void flowRemoved(const QString &flowId);
|
||||
|
||||
private:
|
||||
QHash<QString, Flow *> m_flows;
|
||||
|
||||
TaskRegistry *m_taskRegistry;
|
||||
FlowRegistry *m_flowRegistry;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
43
TaskFlow/core/FlowRegistry.cpp
Normal file
@ -0,0 +1,43 @@
|
||||
#include "FlowRegistry.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowRegistry::FlowRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void FlowRegistry::registerFlow(const QString &flowType, FlowCreator creator)
|
||||
{
|
||||
m_flowCreators[flowType] = creator;
|
||||
LOG_MESSAGE(QString("FlowRegistry: Registered flow type '%1'").arg(flowType));
|
||||
}
|
||||
|
||||
Flow *FlowRegistry::createFlow(const QString &flowType, FlowManager *flowManager) const
|
||||
{
|
||||
LOG_MESSAGE(QString("Trying to create flow: %1").arg(flowType));
|
||||
|
||||
if (m_flowCreators.contains(flowType)) {
|
||||
LOG_MESSAGE(QString("Found creator for flow type: %1").arg(flowType));
|
||||
try {
|
||||
Flow *flow = m_flowCreators[flowType](flowManager);
|
||||
if (flow) {
|
||||
LOG_MESSAGE(QString("Successfully created flow: %1").arg(flowType));
|
||||
return flow;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_MESSAGE(QString("Exception while creating flow of type: %1").arg(flowType));
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("No creator found for flow type: %1").arg(flowType));
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QStringList FlowRegistry::getAvailableTypes() const
|
||||
{
|
||||
return m_flowCreators.keys();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
29
TaskFlow/core/FlowRegistry.hpp
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class Flow;
|
||||
class FlowManager;
|
||||
|
||||
class FlowRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using FlowCreator = std::function<Flow *(FlowManager *flowManager)>;
|
||||
|
||||
explicit FlowRegistry(QObject *parent = nullptr);
|
||||
|
||||
void registerFlow(const QString &flowType, FlowCreator creator);
|
||||
Flow *createFlow(const QString &flowType, FlowManager *flowManager = nullptr) const;
|
||||
QStringList getAvailableTypes() const;
|
||||
|
||||
private:
|
||||
QHash<QString, FlowCreator> m_flowCreators;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
125
TaskFlow/core/TaskConnection.cpp
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TaskConnection.hpp"
|
||||
#include "BaseTask.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
#include <QMetaEnum>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskConnection::TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_sourcePort(sourcePort)
|
||||
, m_targetPort(targetPort)
|
||||
{
|
||||
setupConnection();
|
||||
}
|
||||
|
||||
TaskConnection::~TaskConnection()
|
||||
{
|
||||
cleanupConnection();
|
||||
}
|
||||
|
||||
BaseTask *TaskConnection::sourceTask() const
|
||||
{
|
||||
return m_sourcePort ? qobject_cast<BaseTask *>(m_sourcePort->parent()) : nullptr;
|
||||
}
|
||||
|
||||
BaseTask *TaskConnection::targetTask() const
|
||||
{
|
||||
return m_targetPort ? qobject_cast<BaseTask *>(m_targetPort->parent()) : nullptr;
|
||||
}
|
||||
|
||||
TaskPort *TaskConnection::sourcePort() const
|
||||
{
|
||||
return m_sourcePort;
|
||||
}
|
||||
|
||||
TaskPort *TaskConnection::targetPort() const
|
||||
{
|
||||
return m_targetPort;
|
||||
}
|
||||
|
||||
bool TaskConnection::isValid() const
|
||||
{
|
||||
return m_sourcePort && m_targetPort && m_sourcePort != m_targetPort && sourceTask()
|
||||
&& targetTask() && sourceTask() != targetTask();
|
||||
}
|
||||
|
||||
bool TaskConnection::isTypeCompatible() const
|
||||
{
|
||||
if (!isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_targetPort->isConnectionTypeCompatible(m_sourcePort);
|
||||
}
|
||||
|
||||
QString TaskConnection::toString() const
|
||||
{
|
||||
if (!isValid()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
BaseTask *srcTask = sourceTask();
|
||||
BaseTask *tgtTask = targetTask();
|
||||
|
||||
return QString("%1.%2->%3.%4")
|
||||
.arg(srcTask->taskId())
|
||||
.arg(m_sourcePort->name())
|
||||
.arg(tgtTask->taskId())
|
||||
.arg(m_targetPort->name());
|
||||
}
|
||||
|
||||
bool TaskConnection::operator==(const TaskConnection &other) const
|
||||
{
|
||||
return m_sourcePort == other.m_sourcePort && m_targetPort == other.m_targetPort;
|
||||
}
|
||||
|
||||
void TaskConnection::setupConnection()
|
||||
{
|
||||
if (!isValid()) {
|
||||
qWarning() << "TaskConnection::setupConnection - Invalid connection parameters";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTypeCompatible()) {
|
||||
QMetaEnum metaEnum = QMetaEnum::fromType<TaskPort::ValueType>();
|
||||
qWarning() << "TaskConnection::setupConnection - Type incompatible connection:"
|
||||
<< metaEnum.valueToKey(static_cast<int>(m_sourcePort->valueType())) << "to"
|
||||
<< metaEnum.valueToKey(static_cast<int>(m_targetPort->valueType()));
|
||||
}
|
||||
|
||||
m_sourcePort->setConnection(this);
|
||||
m_targetPort->setConnection(this);
|
||||
}
|
||||
|
||||
void TaskConnection::cleanupConnection()
|
||||
{
|
||||
if (m_sourcePort && m_sourcePort->connection() == this) {
|
||||
m_sourcePort->setConnection(nullptr);
|
||||
}
|
||||
|
||||
if (m_targetPort && m_targetPort->connection() == this) {
|
||||
m_targetPort->setConnection(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
65
TaskFlow/core/TaskConnection.hpp
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class BaseTask;
|
||||
class TaskPort;
|
||||
|
||||
class TaskConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// Constructor automatically sets up the connection
|
||||
explicit TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent = nullptr);
|
||||
|
||||
// Destructor automatically cleans up the connection
|
||||
~TaskConnection() override;
|
||||
|
||||
// Getters
|
||||
BaseTask *sourceTask() const;
|
||||
BaseTask *targetTask() const;
|
||||
TaskPort *sourcePort() const;
|
||||
TaskPort *targetPort() const;
|
||||
|
||||
// Validation
|
||||
bool isValid() const;
|
||||
bool isTypeCompatible() const;
|
||||
|
||||
// Utility
|
||||
QString toString() const;
|
||||
|
||||
// Comparison
|
||||
bool operator==(const TaskConnection &other) const;
|
||||
|
||||
private:
|
||||
TaskPort *m_sourcePort;
|
||||
TaskPort *m_targetPort;
|
||||
|
||||
void setupConnection();
|
||||
void cleanupConnection();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
122
TaskFlow/core/TaskPort.cpp
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TaskPort.hpp"
|
||||
#include "TaskConnection.hpp"
|
||||
#include <QMetaEnum>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskPort::TaskPort(const QString &name, ValueType type, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_name(name)
|
||||
, m_valueType(type)
|
||||
{}
|
||||
|
||||
QString TaskPort::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
void TaskPort::setValueType(ValueType type)
|
||||
{
|
||||
if (m_valueType != type)
|
||||
m_valueType = type;
|
||||
}
|
||||
|
||||
TaskPort::ValueType TaskPort::valueType() const
|
||||
{
|
||||
return m_valueType;
|
||||
}
|
||||
|
||||
void TaskPort::setValue(const QVariant &value)
|
||||
{
|
||||
if (!isValueTypeCompatible(value)) {
|
||||
qWarning() << "TaskPort::setValue - Type mismatch for port" << m_name << "Expected:"
|
||||
<< QMetaEnum::fromType<ValueType>().valueToKey(static_cast<int>(m_valueType))
|
||||
<< "Got:" << value.typeName();
|
||||
}
|
||||
|
||||
if (m_value != value) {
|
||||
m_value = value;
|
||||
emit valueChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant TaskPort::value() const
|
||||
{
|
||||
if (hasConnection() && m_connection->sourcePort()) {
|
||||
return m_connection->sourcePort()->m_value;
|
||||
}
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void TaskPort::setConnection(TaskConnection *connection)
|
||||
{
|
||||
if (m_connection != connection) {
|
||||
m_connection = connection;
|
||||
emit connectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
TaskConnection *TaskPort::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
bool TaskPort::hasConnection() const
|
||||
{
|
||||
return m_connection != nullptr;
|
||||
}
|
||||
|
||||
bool TaskPort::isValueTypeCompatible(const QVariant &value) const
|
||||
{
|
||||
if (m_valueType == ValueType::Any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (m_valueType) {
|
||||
case ValueType::String:
|
||||
return value.canConvert<QString>();
|
||||
|
||||
case ValueType::Number:
|
||||
return value.canConvert<double>() || value.canConvert<int>();
|
||||
|
||||
case ValueType::Boolean:
|
||||
return value.canConvert<bool>();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskPort::isConnectionTypeCompatible(const TaskPort *sourcePort) const
|
||||
{
|
||||
if (!sourcePort) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourcePort->valueType() == ValueType::Any || m_valueType == ValueType::Any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return sourcePort->valueType() == m_valueType;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
75
TaskFlow/core/TaskPort.hpp
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include "TaskConnection.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskPort : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class ValueType {
|
||||
Any, // QVariant
|
||||
String, // QString
|
||||
Number, // int/double
|
||||
Boolean // bool
|
||||
};
|
||||
Q_ENUM(ValueType)
|
||||
|
||||
explicit TaskPort(
|
||||
const QString &name, ValueType type = ValueType::Any, QObject *parent = nullptr);
|
||||
|
||||
QString name() const;
|
||||
|
||||
ValueType valueType() const;
|
||||
void setValueType(ValueType type);
|
||||
|
||||
void setValue(const QVariant &value);
|
||||
QVariant value() const;
|
||||
|
||||
void setConnection(TaskConnection *connection);
|
||||
TaskConnection *connection() const;
|
||||
bool hasConnection() const;
|
||||
|
||||
bool isValueTypeCompatible(const QVariant &value) const;
|
||||
bool isConnectionTypeCompatible(const TaskPort *sourcePort) const;
|
||||
|
||||
signals:
|
||||
void valueChanged();
|
||||
void connectionChanged();
|
||||
|
||||
private:
|
||||
QString m_name;
|
||||
ValueType m_valueType;
|
||||
QVariant m_value;
|
||||
TaskConnection *m_connection = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
|
||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort *)
|
||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort::ValueType)
|
||||
59
TaskFlow/core/TaskRegistry.cpp
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TaskRegistry.hpp"
|
||||
|
||||
#include <Logger.hpp>
|
||||
|
||||
#include "BaseTask.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskRegistry::TaskRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
BaseTask *TaskRegistry::createTask(const QString &taskType, QObject *parent) const
|
||||
{
|
||||
LOG_MESSAGE(QString("Trying to create task: %1").arg(taskType));
|
||||
|
||||
if (m_creators.contains(taskType)) {
|
||||
LOG_MESSAGE(QString("Found creator for task type: %1").arg(taskType));
|
||||
try {
|
||||
BaseTask *task = m_creators[taskType](parent);
|
||||
if (task) {
|
||||
LOG_MESSAGE(QString("Successfully created task: %1").arg(taskType));
|
||||
return task;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG_MESSAGE(QString("Exception while creating task of type: %1").arg(taskType));
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("No creator found for task type: %1").arg(taskType));
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QStringList TaskRegistry::getAvailableTypes() const
|
||||
{
|
||||
return m_creators.keys();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
51
TaskFlow/core/TaskRegistry.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class BaseTask;
|
||||
|
||||
class TaskRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using TaskCreator = std::function<BaseTask *(QObject *parent)>;
|
||||
|
||||
explicit TaskRegistry(QObject *parent = nullptr);
|
||||
|
||||
template<typename T>
|
||||
inline void registerTask(const QString &taskType)
|
||||
{
|
||||
m_creators[taskType] = [](QObject *parent) -> BaseTask * { return new T(parent); };
|
||||
}
|
||||
BaseTask *createTask(const QString &taskType, QObject *parent = nullptr) const;
|
||||
QStringList getAvailableTypes() const;
|
||||
|
||||
private:
|
||||
QHash<QString, TaskCreator> m_creators;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
@ -33,6 +33,9 @@ UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
|
||||
m_actionButton = new QToolButton(this);
|
||||
m_actionButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
|
||||
|
||||
m_chatButton = new QToolButton(this);
|
||||
m_chatButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
|
||||
|
||||
m_versionLabel = new QLabel(this);
|
||||
m_versionLabel->setVisible(false);
|
||||
|
||||
@ -41,6 +44,7 @@ UpdateStatusWidget::UpdateStatusWidget(QWidget *parent)
|
||||
m_updateButton->setStyleSheet("QPushButton { padding: 2px 8px; }");
|
||||
|
||||
layout->addWidget(m_actionButton);
|
||||
layout->addWidget(m_chatButton);
|
||||
layout->addWidget(m_versionLabel);
|
||||
layout->addWidget(m_updateButton);
|
||||
}
|
||||
@ -64,6 +68,11 @@ void UpdateStatusWidget::hideUpdateInfo()
|
||||
m_updateButton->setVisible(false);
|
||||
}
|
||||
|
||||
void UpdateStatusWidget::setChatButtonAction(QAction *action)
|
||||
{
|
||||
m_chatButton->setDefaultAction(action);
|
||||
}
|
||||
|
||||
QPushButton *UpdateStatusWidget::updateButton() const
|
||||
{
|
||||
return m_updateButton;
|
||||
|
||||
@ -36,11 +36,13 @@ public:
|
||||
void setDefaultAction(QAction *action);
|
||||
void showUpdateAvailable(const QString &version);
|
||||
void hideUpdateInfo();
|
||||
void setChatButtonAction(QAction *action);
|
||||
|
||||
QPushButton *updateButton() const;
|
||||
|
||||
private:
|
||||
QToolButton *m_actionButton;
|
||||
QToolButton *m_chatButton;
|
||||
QLabel *m_versionLabel;
|
||||
QPushButton *m_updateButton;
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ add_library(LLMCore STATIC
|
||||
OpenAIMessage.hpp OpenAIMessage.cpp
|
||||
ValidationUtils.hpp ValidationUtils.cpp
|
||||
ProviderID.hpp
|
||||
HttpClient.hpp HttpClient.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(LLMCore
|
||||
|
||||
150
llmcore/HttpClient.cpp
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "HttpClient.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QMutexLocker>
|
||||
#include <QUuid>
|
||||
|
||||
#include <Logger.hpp>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
HttpClient::HttpClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest);
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
for (auto *reply : std::as_const(m_activeRequests)) {
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_activeRequests.clear();
|
||||
}
|
||||
|
||||
void HttpClient::onSendRequest(const HttpRequest &request)
|
||||
{
|
||||
QNetworkRequest networkRequest(request.url);
|
||||
networkRequest.setTransferTimeout(300000);
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
networkRequest.setRawHeader("Accept", "text/event-stream");
|
||||
networkRequest.setRawHeader("Cache-Control", "no-cache");
|
||||
networkRequest.setRawHeader("Connection", "keep-alive");
|
||||
|
||||
if (request.headers.has_value()) {
|
||||
for (const auto &[headername, value] : request.headers->asKeyValueRange()) {
|
||||
networkRequest.setRawHeader(headername.toUtf8(), value.toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
QJsonDocument doc(request.payload);
|
||||
LOG_MESSAGE(QString("HttpClient: Sending POST to %1").arg(request.url.toString()));
|
||||
LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented)));
|
||||
|
||||
QNetworkReply *reply = m_manager->post(networkRequest, doc.toJson(QJsonDocument::Compact));
|
||||
addActiveRequest(reply, request.requestId);
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
|
||||
connect(reply, &QNetworkReply::finished, this, &HttpClient::onFinished);
|
||||
}
|
||||
|
||||
void HttpClient::onReadyRead()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
QString requestId;
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value() == reply) {
|
||||
requestId = it.key();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestId.isEmpty())
|
||||
return;
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
if (!data.isEmpty()) {
|
||||
emit dataReceived(requestId, data);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpClient::onFinished()
|
||||
{
|
||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
QString requestId;
|
||||
bool hasError = false;
|
||||
QString errorMsg;
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value() == reply) {
|
||||
requestId = it.key();
|
||||
m_activeRequests.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
hasError = true;
|
||||
errorMsg = reply->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!requestId.isEmpty()) {
|
||||
emit requestFinished(requestId, !hasError, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
QString HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_activeRequests[requestId] = reply;
|
||||
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
|
||||
return requestId;
|
||||
}
|
||||
|
||||
void HttpClient::cancelRequest(const QString &requestId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
if (auto it = m_activeRequests.find(requestId); it != m_activeRequests.end()) {
|
||||
it.value()->abort();
|
||||
it.value()->deleteLater();
|
||||
m_activeRequests.erase(it);
|
||||
LOG_MESSAGE(QString("HttpClient: Cancelled request: %1").arg(requestId));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
69
llmcore/HttpClient.hpp
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
QUrl url;
|
||||
QString requestId;
|
||||
QJsonObject payload;
|
||||
std::optional<QMap<QString, QString>> headers;
|
||||
};
|
||||
|
||||
class HttpClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HttpClient(QObject *parent = nullptr);
|
||||
~HttpClient();
|
||||
|
||||
void cancelRequest(const QString &requestId);
|
||||
|
||||
signals:
|
||||
void sendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
void dataReceived(const QString &requestId, const QByteArray &data);
|
||||
void requestFinished(const QString &requestId, bool success, const QString &error);
|
||||
|
||||
private slots:
|
||||
void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
void onReadyRead();
|
||||
void onFinished();
|
||||
|
||||
private:
|
||||
QString addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
||||
|
||||
QNetworkAccessManager *m_manager;
|
||||
QHash<QString, QNetworkReply *> m_activeRequests;
|
||||
mutable QMutex m_mutex;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
enum class TemplateType { Chat, FIM };
|
||||
enum class TemplateType { Chat, FIM, FIMOnChat };
|
||||
|
||||
class PromptTemplate
|
||||
{
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
#include "PromptTemplateManager.hpp"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
PromptTemplateManager &PromptTemplateManager::instance()
|
||||
@ -70,15 +72,27 @@ PromptTemplateManager::~PromptTemplateManager()
|
||||
|
||||
PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName)
|
||||
{
|
||||
if (!m_fimTemplates.contains(templateName))
|
||||
if (!m_fimTemplates.contains(templateName)) {
|
||||
QMessageBox::warning(
|
||||
nullptr,
|
||||
QObject::tr("Template Not Found"),
|
||||
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
|
||||
.arg(templateName));
|
||||
return m_fimTemplates.first();
|
||||
}
|
||||
return m_fimTemplates[templateName];
|
||||
}
|
||||
|
||||
PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName)
|
||||
{
|
||||
if (!m_chatTemplates.contains(templateName))
|
||||
if (!m_chatTemplates.contains(templateName)) {
|
||||
QMessageBox::warning(
|
||||
nullptr,
|
||||
QObject::tr("Template Not Found"),
|
||||
QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.")
|
||||
.arg(templateName));
|
||||
return m_chatTemplates.first();
|
||||
}
|
||||
return m_chatTemplates[templateName];
|
||||
}
|
||||
|
||||
|
||||
@ -22,14 +22,51 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
RequestHandler::RequestHandler(QObject *parent)
|
||||
: RequestHandlerBase(parent)
|
||||
{}
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(
|
||||
this,
|
||||
&RequestHandler::doSendRequest,
|
||||
this,
|
||||
&RequestHandler::sendLLMRequestInternal,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
this,
|
||||
&RequestHandler::doCancelRequest,
|
||||
this,
|
||||
&RequestHandler::cancelRequestInternal,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
RequestHandler::~RequestHandler()
|
||||
{
|
||||
for (auto reply : m_activeRequests) {
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
}
|
||||
|
||||
void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
emit doSendRequest(config, request);
|
||||
}
|
||||
|
||||
bool RequestHandler::cancelRequest(const QString &id)
|
||||
{
|
||||
emit doCancelRequest(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RequestHandler::sendLLMRequestInternal(const LLMConfig &config, const QJsonObject &request)
|
||||
{
|
||||
LOG_MESSAGE(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
|
||||
.arg(
|
||||
@ -37,12 +74,13 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(config.providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager();
|
||||
QNetworkRequest networkRequest(config.url);
|
||||
networkRequest.setTransferTimeout(300000);
|
||||
|
||||
config.provider->prepareNetworkRequest(networkRequest);
|
||||
|
||||
QNetworkReply *reply
|
||||
= manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson());
|
||||
= m_manager->post(networkRequest, QJsonDocument(config.providerRequest).toJson());
|
||||
if (!reply) {
|
||||
LOG_MESSAGE("Error: Failed to create network reply");
|
||||
return;
|
||||
@ -55,24 +93,28 @@ void RequestHandler::sendLLMRequest(const LLMConfig &config, const QJsonObject &
|
||||
handleLLMResponse(reply, request, config);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId, manager]() {
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMessage = reply->errorString();
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
connect(
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[this, reply, requestId]() {
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QString errorMessage = reply->errorString();
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Error details: %1\nStatus code: %2").arg(errorMessage).arg(statusCode));
|
||||
LOG_MESSAGE(
|
||||
QString("Error details: %1\nStatus code: %2").arg(errorMessage).arg(statusCode));
|
||||
|
||||
emit requestFinished(requestId, false, errorMessage);
|
||||
} else {
|
||||
LOG_MESSAGE("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
emit requestFinished(requestId, false, errorMessage);
|
||||
} else {
|
||||
LOG_MESSAGE("Request finished successfully");
|
||||
emit requestFinished(requestId, true, QString());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
});
|
||||
reply->deleteLater();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void RequestHandler::handleLLMResponse(
|
||||
@ -102,17 +144,27 @@ void RequestHandler::handleLLMResponse(
|
||||
m_accumulatedResponses.remove(reply);
|
||||
}
|
||||
|
||||
bool RequestHandler::cancelRequest(const QString &id)
|
||||
void RequestHandler::cancelRequestInternal(const QString &id)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
if (m_activeRequests.contains(id)) {
|
||||
QNetworkReply *reply = m_activeRequests[id];
|
||||
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
|
||||
reply->abort();
|
||||
m_activeRequests.remove(id);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
locker.unlock();
|
||||
|
||||
m_manager->clearConnectionCache();
|
||||
m_manager->clearAccessCache();
|
||||
|
||||
emit requestCancelled(id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RequestHandler::processSingleLineCompletion(
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
@ -32,16 +33,32 @@ namespace QodeAssist::LLMCore {
|
||||
|
||||
class RequestHandler : public RequestHandlerBase
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RequestHandler(QObject *parent = nullptr);
|
||||
~RequestHandler() override;
|
||||
|
||||
void sendLLMRequest(const LLMConfig &config, const QJsonObject &request) override;
|
||||
bool cancelRequest(const QString &id) override;
|
||||
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request, const LLMConfig &config);
|
||||
|
||||
signals:
|
||||
void doSendRequest(QodeAssist::LLMCore::LLMConfig config, QJsonObject request);
|
||||
void doCancelRequest(QString id);
|
||||
|
||||
private slots:
|
||||
void sendLLMRequestInternal(
|
||||
const QodeAssist::LLMCore::LLMConfig &config, const QJsonObject &request);
|
||||
void cancelRequestInternal(const QString &id);
|
||||
void handleLLMResponse(
|
||||
QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QodeAssist::LLMCore::LLMConfig &config);
|
||||
|
||||
private:
|
||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||
QNetworkAccessManager *m_manager;
|
||||
QMutex m_mutex;
|
||||
|
||||
bool processSingleLineCompletion(
|
||||
QNetworkReply *reply,
|
||||
|
||||
@ -45,7 +45,7 @@ QString LMStudioProvider::url() const
|
||||
|
||||
QString LMStudioProvider::completionEndpoint() const
|
||||
{
|
||||
return "/v1/chat/completions";
|
||||
return "/v1/completions";
|
||||
}
|
||||
|
||||
QString LMStudioProvider::chatEndpoint() const
|
||||
|
||||
@ -40,7 +40,9 @@
|
||||
#include <QMainWindow>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QTranslator>
|
||||
|
||||
#include <QInputDialog>
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "QodeAssistClient.hpp"
|
||||
#include "UpdateStatusWidget.hpp"
|
||||
@ -52,17 +54,18 @@
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "logger/RequestPerformanceLogger.hpp"
|
||||
#include "providers/Providers.hpp"
|
||||
#include "settings/ChatAssistantSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettingsPanel.hpp"
|
||||
#include "settings/SettingsConstants.hpp"
|
||||
#include "templates/Templates.hpp"
|
||||
#include "widgets/QuickRefactorDialog.hpp"
|
||||
#include <ChatView/ChatView.hpp>
|
||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/texteditorconstants.h>
|
||||
#include <QInputDialog>
|
||||
|
||||
using namespace Utils;
|
||||
using namespace Core;
|
||||
@ -85,8 +88,29 @@ public:
|
||||
~QodeAssistPlugin() final
|
||||
{
|
||||
delete m_qodeAssistClient;
|
||||
delete m_chatOutputPane;
|
||||
delete m_navigationPanel;
|
||||
if (m_chatOutputPane) {
|
||||
delete m_chatOutputPane;
|
||||
}
|
||||
if (m_navigationPanel) {
|
||||
delete m_navigationPanel;
|
||||
}
|
||||
}
|
||||
|
||||
void loadTranslations()
|
||||
{
|
||||
const QString langId = Core::ICore::userInterfaceLanguage();
|
||||
|
||||
QTranslator *translator = new QTranslator(qApp);
|
||||
QString resourcePath = QString(":/translations/QodeAssist_%1.qm").arg(langId);
|
||||
|
||||
bool success = translator->load(resourcePath);
|
||||
if (success) {
|
||||
qApp->installTranslator(translator);
|
||||
qDebug() << "Loaded translation from resources:" << resourcePath;
|
||||
} else {
|
||||
delete translator;
|
||||
qDebug() << "No translation found for language:" << langId;
|
||||
}
|
||||
}
|
||||
|
||||
void initialize() final
|
||||
@ -97,12 +121,15 @@ public:
|
||||
Constants::QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY,
|
||||
":/resources/images/qoderassist-icon.png");
|
||||
#endif
|
||||
loadTranslations();
|
||||
|
||||
Providers::registerProviders();
|
||||
Templates::registerTemplates();
|
||||
|
||||
Utils::Icon QCODEASSIST_ICON(
|
||||
{{":/resources/images/qoderassist-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
Utils::Icon QCODEASSIST_CHAT_ICON(
|
||||
{{":/resources/images/qode-assist-chat-icon.png", Utils::Theme::IconsBaseColor}});
|
||||
|
||||
ActionBuilder requestAction(this, Constants::QODE_ASSIST_REQUEST_SUGGESTION);
|
||||
requestAction.setToolTip(
|
||||
@ -129,8 +156,10 @@ public:
|
||||
UpdateDialog::checkForUpdatesAndShow(Core::ICore::mainWindow());
|
||||
});
|
||||
|
||||
if (Settings::generalSettings().enableChat()) {
|
||||
if (Settings::chatAssistantSettings().enableChatInBottomToolBar()) {
|
||||
m_chatOutputPane = new Chat::ChatOutputPane(this);
|
||||
}
|
||||
if (Settings::chatAssistantSettings().enableChatInNavigationPanel()) {
|
||||
m_navigationPanel = new Chat::NavigationPanel();
|
||||
}
|
||||
|
||||
@ -167,6 +196,34 @@ public:
|
||||
}
|
||||
});
|
||||
|
||||
ActionBuilder showChatViewAction(this, "QodeAssist.ShowChatView");
|
||||
const QKeySequence showChatViewShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_W);
|
||||
showChatViewAction.setDefaultKeySequence(showChatViewShortcut);
|
||||
showChatViewAction.setToolTip(Tr::tr("Show QodeAssist Chat"));
|
||||
showChatViewAction.setText(Tr::tr("Show QodeAssist Chat"));
|
||||
showChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
||||
showChatViewAction.addOnTriggered(this, [this] {
|
||||
if (!m_chatView->isVisible()) {
|
||||
m_chatView->show();
|
||||
}
|
||||
|
||||
m_chatView->raise();
|
||||
m_chatView->requestActivate();
|
||||
});
|
||||
m_statusWidget->setChatButtonAction(showChatViewAction.contextAction());
|
||||
|
||||
ActionBuilder closeChatViewAction(this, "QodeAssist.CloseChatView");
|
||||
const QKeySequence closeChatViewShortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_S);
|
||||
closeChatViewAction.setDefaultKeySequence(closeChatViewShortcut);
|
||||
closeChatViewAction.setToolTip(Tr::tr("Close QodeAssist Chat"));
|
||||
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
|
||||
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
||||
closeChatViewAction.addOnTriggered(this, [this] {
|
||||
if (m_chatView->isVisible()) {
|
||||
m_chatView->close();
|
||||
}
|
||||
});
|
||||
|
||||
Core::ActionContainer *editorContextMenu = Core::ActionManager::actionContainer(
|
||||
TextEditor::Constants::M_STANDARDCONTEXTMENU);
|
||||
if (editorContextMenu) {
|
||||
@ -174,10 +231,14 @@ public:
|
||||
editorContextMenu
|
||||
->addAction(quickRefactorAction.command(), Core::Constants::G_DEFAULT_THREE);
|
||||
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
|
||||
editorContextMenu->addAction(showChatViewAction.command(),
|
||||
Core::Constants::G_DEFAULT_THREE);
|
||||
editorContextMenu->addAction(closeChatViewAction.command(),
|
||||
Core::Constants::G_DEFAULT_THREE);
|
||||
}
|
||||
}
|
||||
|
||||
void extensionsInitialized() final {}
|
||||
void extensionsInitialized() final { m_chatView.reset(new Chat::ChatView()); }
|
||||
|
||||
void restartClient()
|
||||
{
|
||||
@ -237,6 +298,7 @@ private:
|
||||
QPointer<PluginUpdater> m_updater;
|
||||
UpdateStatusWidget *m_statusWidget{nullptr};
|
||||
QString m_lastRefactorInstructions;
|
||||
QScopedPointer<Chat::ChatView> m_chatView;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Internal
|
||||
|
||||
BIN
resources/images/qode-assist-chat-icon.png
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
resources/images/qode-assist-chat-icon@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
1017
resources/translations/QodeAssist_en.ts
Normal file
@ -22,6 +22,8 @@
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <QApplication>
|
||||
#include <QFontDatabase>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "SettingsConstants.hpp"
|
||||
@ -47,8 +49,8 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
chatTokensThreshold.setLabelText(Tr::tr("Chat history token limit:"));
|
||||
chatTokensThreshold.setToolTip(Tr::tr("Maximum number of tokens in chat history. When "
|
||||
"exceeded, oldest messages will be removed."));
|
||||
chatTokensThreshold.setRange(1, std::numeric_limits<qint64>::max());
|
||||
chatTokensThreshold.setDefaultValue(8000);
|
||||
chatTokensThreshold.setRange(1, 99999999);
|
||||
chatTokensThreshold.setDefaultValue(20000);
|
||||
|
||||
linkOpenFiles.setSettingsKey(Constants::CA_LINK_OPEN_FILES);
|
||||
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
|
||||
@ -62,6 +64,14 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
enableChatInBottomToolBar.setSettingsKey(Constants::CA_ENABLE_CHAT_IN_BOTTOM_TOOLBAR);
|
||||
enableChatInBottomToolBar.setLabelText(Tr::tr("Enable chat in bottom toolbar"));
|
||||
enableChatInBottomToolBar.setDefaultValue(false);
|
||||
|
||||
enableChatInNavigationPanel.setSettingsKey(Constants::CA_ENABLE_CHAT_IN_NAVIGATION_PANEL);
|
||||
enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel"));
|
||||
enableChatInNavigationPanel.setDefaultValue(false);
|
||||
|
||||
// General Parameters Settings
|
||||
temperature.setSettingsKey(Constants::CA_TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
@ -137,6 +147,65 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
contextWindow.setRange(-1, 10000);
|
||||
contextWindow.setDefaultValue(2048);
|
||||
|
||||
autosave.setDefaultValue(true);
|
||||
autosave.setLabelText(Tr::tr("Enable autosave when message received"));
|
||||
|
||||
textFontFamily.setSettingsKey(Constants::CA_TEXT_FONT_FAMILY);
|
||||
textFontFamily.setLabelText(Tr::tr("Text Font:"));
|
||||
textFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList families = QFontDatabase::families();
|
||||
for (const QString &family : families) {
|
||||
textFontFamily.addOption(family);
|
||||
}
|
||||
textFontFamily.setDefaultValue(QApplication::font().family());
|
||||
|
||||
textFontSize.setSettingsKey(Constants::CA_TEXT_FONT_SIZE);
|
||||
textFontSize.setLabelText(Tr::tr("Text Font Size:"));
|
||||
textFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
codeFontFamily.setSettingsKey(Constants::CA_CODE_FONT_FAMILY);
|
||||
codeFontFamily.setLabelText(Tr::tr("Code Font:"));
|
||||
codeFontFamily.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
const QStringList monospaceFamilies = QFontDatabase::families(QFontDatabase::Latin);
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
codeFontFamily.addOption(family);
|
||||
}
|
||||
}
|
||||
|
||||
QString defaultMonoFont;
|
||||
QStringList fixedPitchFamilies;
|
||||
|
||||
for (const QString &family : monospaceFamilies) {
|
||||
if (QFontDatabase::isFixedPitch(family)) {
|
||||
fixedPitchFamilies.append(family);
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedPitchFamilies.contains("Consolas")) {
|
||||
defaultMonoFont = "Consolas";
|
||||
} else if (fixedPitchFamilies.contains("Courier New")) {
|
||||
defaultMonoFont = "Courier New";
|
||||
} else if (fixedPitchFamilies.contains("Monospace")) {
|
||||
defaultMonoFont = "Monospace";
|
||||
} else if (!fixedPitchFamilies.isEmpty()) {
|
||||
defaultMonoFont = fixedPitchFamilies.first();
|
||||
} else {
|
||||
defaultMonoFont = QApplication::font().family();
|
||||
}
|
||||
codeFontFamily.setDefaultValue(defaultMonoFont);
|
||||
codeFontSize.setSettingsKey(Constants::CA_CODE_FONT_SIZE);
|
||||
codeFontSize.setLabelText(Tr::tr("Code Font Size:"));
|
||||
codeFontSize.setDefaultValue(QApplication::font().pointSize());
|
||||
|
||||
textFormat.setSettingsKey(Constants::CA_TEXT_FORMAT);
|
||||
textFormat.setLabelText(Tr::tr("Text Format:"));
|
||||
textFormat.setDefaultValue(0);
|
||||
textFormat.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
textFormat.addOption("Markdown");
|
||||
textFormat.addOption("HTML");
|
||||
textFormat.addOption("Plain Text");
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
|
||||
readSettings();
|
||||
@ -160,28 +229,37 @@ ChatAssistantSettings::ChatAssistantSettings()
|
||||
ollamaGrid.addRow({ollamaLivetime});
|
||||
ollamaGrid.addRow({contextWindow});
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Chat Settings")),
|
||||
Column{Row{chatTokensThreshold, Stretch{1}}, linkOpenFiles, stream, autosave}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Context Settings")),
|
||||
Column{
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
auto chatViewSettingsGrid = Grid{};
|
||||
chatViewSettingsGrid.addRow({textFontFamily, textFontSize});
|
||||
chatViewSettingsGrid.addRow({codeFontFamily, codeFontSize});
|
||||
chatViewSettingsGrid.addRow({textFormat});
|
||||
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Chat Settings")),
|
||||
Column{Row{chatTokensThreshold, Stretch{1}},
|
||||
linkOpenFiles,
|
||||
stream,
|
||||
autosave,
|
||||
enableChatInBottomToolBar,
|
||||
enableChatInNavigationPanel}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Row{genGrid, Stretch{1}},
|
||||
},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")),
|
||||
Column{
|
||||
Row{useSystemPrompt, Stretch{1}},
|
||||
systemPrompt,
|
||||
}},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,6 +299,11 @@ void ChatAssistantSettings::resetSettingsToDefaults()
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(contextWindow);
|
||||
resetAspect(linkOpenFiles);
|
||||
resetAspect(textFontFamily);
|
||||
resetAspect(codeFontFamily);
|
||||
resetAspect(textFontSize);
|
||||
resetAspect(codeFontSize);
|
||||
resetAspect(textFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,8 @@ public:
|
||||
Utils::BoolAspect linkOpenFiles{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::BoolAspect autosave{this};
|
||||
Utils::BoolAspect enableChatInBottomToolBar{this};
|
||||
Utils::BoolAspect enableChatInNavigationPanel{this};
|
||||
|
||||
// General Parameters Settings
|
||||
Utils::DoubleAspect temperature{this};
|
||||
@ -63,6 +65,13 @@ public:
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::IntegerAspect contextWindow{this};
|
||||
|
||||
// Visuals settings
|
||||
Utils::SelectionAspect textFontFamily{this};
|
||||
Utils::IntegerAspect textFontSize{this};
|
||||
Utils::SelectionAspect codeFontFamily{this};
|
||||
Utils::IntegerAspect codeFontSize{this};
|
||||
Utils::SelectionAspect textFormat{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetSettingsToDefaults();
|
||||
|
||||
@ -55,9 +55,19 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
stream.setDefaultValue(true);
|
||||
stream.setLabelText(Tr::tr("Enable stream option"));
|
||||
|
||||
smartProcessInstuctText.setSettingsKey(Constants::CC_SMART_PROCESS_INSTRUCT_TEXT);
|
||||
smartProcessInstuctText.setDefaultValue(true);
|
||||
smartProcessInstuctText.setLabelText(Tr::tr("Enable smart process text from instruct model"));
|
||||
modelOutputHandler.setLabelText(Tr::tr("Text output proccessing mode:"));
|
||||
modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER);
|
||||
modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
modelOutputHandler.addOption("Auto");
|
||||
modelOutputHandler.addOption("Force processing");
|
||||
modelOutputHandler.addOption("Raw text");
|
||||
modelOutputHandler.setDefaultValue("Auto");
|
||||
modelOutputHandler.setToolTip(
|
||||
Tr::tr("Auto: Automatically detects codeblock and applies processing when found, other "
|
||||
"text as comments\n"
|
||||
"Force Processing: Always processes text with codeblock formatting and other text "
|
||||
"as comments\n"
|
||||
"Raw Text: Shows unprocessed text without any formatting"));
|
||||
|
||||
startSuggestionTimer.setSettingsKey(Constants::СС_START_SUGGESTION_TIMER);
|
||||
startSuggestionTimer.setLabelText(Tr::tr("with delay(ms)"));
|
||||
@ -287,40 +297,36 @@ CodeCompletionSettings::CodeCompletionSettings()
|
||||
}},
|
||||
Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}};
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
||||
Column{
|
||||
autoCompletion,
|
||||
Space{8},
|
||||
multiLineCompletion,
|
||||
stream,
|
||||
smartProcessInstuctText,
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
startSuggestionTimer,
|
||||
Stretch{1}},
|
||||
showProgressWidget,
|
||||
useOpenFilesContext}},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")), Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Quick Refactor Settings")),
|
||||
Column{useOpenFilesInQuickRefactor, quickRefactorSystemPrompt}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
return Column{Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{title(TrConstants::AUTO_COMPLETION_SETTINGS),
|
||||
Column{autoCompletion,
|
||||
Space{8},
|
||||
multiLineCompletion,
|
||||
stream,
|
||||
Row{modelOutputHandler, Stretch{1}},
|
||||
Row{autoCompletionCharThreshold,
|
||||
autoCompletionTypingInterval,
|
||||
startSuggestionTimer,
|
||||
Stretch{1}},
|
||||
showProgressWidget,
|
||||
useOpenFilesContext}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("General Parameters")),
|
||||
Column{
|
||||
Row{genGrid, Stretch{1}},
|
||||
}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Advanced Parameters")),
|
||||
Column{Row{advancedGrid, Stretch{1}}}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Context Settings")), contextItem},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Quick Refactor Settings")),
|
||||
Column{useOpenFilesInQuickRefactor, quickRefactorSystemPrompt}},
|
||||
Space{8},
|
||||
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}},
|
||||
Stretch{1}};
|
||||
});
|
||||
}
|
||||
|
||||
@ -360,7 +366,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
resetAspect(autoCompletion);
|
||||
resetAspect(multiLineCompletion);
|
||||
resetAspect(stream);
|
||||
resetAspect(smartProcessInstuctText);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(useTopP);
|
||||
@ -389,6 +394,7 @@ void CodeCompletionSettings::resetSettingsToDefaults()
|
||||
resetAspect(useOpenFilesContext);
|
||||
resetAspect(useOpenFilesInQuickRefactor);
|
||||
resetAspect(quickRefactorSystemPrompt);
|
||||
resetAspect(modelOutputHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ public:
|
||||
Utils::BoolAspect autoCompletion{this};
|
||||
Utils::BoolAspect multiLineCompletion{this};
|
||||
Utils::BoolAspect stream{this};
|
||||
Utils::BoolAspect smartProcessInstuctText{this};
|
||||
Utils::SelectionAspect modelOutputHandler{this};
|
||||
|
||||
Utils::IntegerAspect startSuggestionTimer{this};
|
||||
Utils::IntegerAspect autoCompletionCharThreshold{this};
|
||||
|
||||
@ -79,10 +79,6 @@ GeneralSettings::GeneralSettings()
|
||||
enableCheckUpdate.setLabelText(TrConstants::ENABLE_CHECK_UPDATE_ON_START);
|
||||
enableCheckUpdate.setDefaultValue(true);
|
||||
|
||||
enableChat.setSettingsKey(Constants::ENABLE_CHAT);
|
||||
enableChat.setLabelText(TrConstants::ENABLE_CHAT);
|
||||
enableChat.setDefaultValue(true);
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
|
||||
|
||||
@ -99,9 +95,20 @@ GeneralSettings::GeneralSettings()
|
||||
ccSelectTemplate.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
initStringAspect(ccUrl, Constants::CC_URL, TrConstants::URL, "http://localhost:11434");
|
||||
ccUrl.setHistoryCompleter(Constants::CC_URL_HISTORY);
|
||||
ccUrl.setHistoryCompleter(Constants::CC_CUSTOM_ENDPOINT_HISTORY);
|
||||
ccSetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
ccEndpointMode.setSettingsKey(Constants::CC_ENDPOINT_MODE);
|
||||
ccEndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
ccEndpointMode.addOption("Auto");
|
||||
ccEndpointMode.addOption("Custom");
|
||||
ccEndpointMode.addOption("FIM");
|
||||
ccEndpointMode.addOption("Chat");
|
||||
ccEndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(ccCustomEndpoint, Constants::CC_CUSTOM_ENDPOINT, TrConstants::ENDPOINT_MODE, "");
|
||||
ccCustomEndpoint.setHistoryCompleter(Constants::CC_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
ccStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay);
|
||||
ccStatus.setLabelText(TrConstants::STATUS);
|
||||
ccStatus.setDefaultValue("");
|
||||
@ -134,6 +141,21 @@ GeneralSettings::GeneralSettings()
|
||||
ccPreset1Url.setHistoryCompleter(Constants::CC_PRESET1_URL_HISTORY);
|
||||
ccPreset1SetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
ccPreset1EndpointMode.setSettingsKey(Constants::CC_PRESET1_ENDPOINT_MODE);
|
||||
ccPreset1EndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
ccPreset1EndpointMode.addOption("Auto");
|
||||
ccPreset1EndpointMode.addOption("Custom");
|
||||
ccPreset1EndpointMode.addOption("FIM");
|
||||
ccPreset1EndpointMode.addOption("Chat");
|
||||
ccPreset1EndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(
|
||||
ccPreset1CustomEndpoint,
|
||||
Constants::CC_PRESET1_CUSTOM_ENDPOINT,
|
||||
TrConstants::ENDPOINT_MODE,
|
||||
"");
|
||||
ccPreset1CustomEndpoint.setHistoryCompleter(Constants::CC_PRESET1_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
initStringAspect(
|
||||
ccPreset1Model, Constants::CC_PRESET1_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b");
|
||||
ccPreset1Model.setHistoryCompleter(Constants::CC_PRESET1_MODEL_HISTORY);
|
||||
@ -162,6 +184,17 @@ GeneralSettings::GeneralSettings()
|
||||
caUrl.setHistoryCompleter(Constants::CA_URL_HISTORY);
|
||||
caSetUrl.m_buttonText = TrConstants::SELECT;
|
||||
|
||||
caEndpointMode.setSettingsKey(Constants::CA_ENDPOINT_MODE);
|
||||
caEndpointMode.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
caEndpointMode.addOption("Auto");
|
||||
caEndpointMode.addOption("Custom");
|
||||
caEndpointMode.addOption("FIM");
|
||||
caEndpointMode.addOption("Chat");
|
||||
caEndpointMode.setDefaultValue("Auto");
|
||||
|
||||
initStringAspect(caCustomEndpoint, Constants::CA_CUSTOM_ENDPOINT, TrConstants::ENDPOINT_MODE, "");
|
||||
caCustomEndpoint.setHistoryCompleter(Constants::CA_CUSTOM_ENDPOINT_HISTORY);
|
||||
|
||||
caStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay);
|
||||
caStatus.setLabelText(TrConstants::STATUS);
|
||||
caStatus.setDefaultValue("");
|
||||
@ -179,6 +212,9 @@ GeneralSettings::GeneralSettings()
|
||||
setupConnections();
|
||||
|
||||
updatePreset1Visiblity(specifyPreset1.value());
|
||||
ccCustomEndpoint.setEnabled(ccEndpointMode.stringValue() == "Custom");
|
||||
ccPreset1CustomEndpoint.setEnabled(ccPreset1EndpointMode.stringValue() == "Custom");
|
||||
caCustomEndpoint.setEnabled(caEndpointMode.stringValue() == "Custom");
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
@ -186,18 +222,21 @@ GeneralSettings::GeneralSettings()
|
||||
auto ccGrid = Grid{};
|
||||
ccGrid.addRow({ccProvider, ccSelectProvider});
|
||||
ccGrid.addRow({ccUrl, ccSetUrl});
|
||||
ccGrid.addRow({ccCustomEndpoint, ccEndpointMode});
|
||||
ccGrid.addRow({ccModel, ccSelectModel});
|
||||
ccGrid.addRow({ccTemplate, ccSelectTemplate});
|
||||
|
||||
auto ccPreset1Grid = Grid{};
|
||||
ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider});
|
||||
ccPreset1Grid.addRow({ccPreset1Url, ccPreset1SetUrl});
|
||||
ccPreset1Grid.addRow({ccPreset1CustomEndpoint, ccPreset1EndpointMode});
|
||||
ccPreset1Grid.addRow({ccPreset1Model, ccPreset1SelectModel});
|
||||
ccPreset1Grid.addRow({ccPreset1Template, ccPreset1SelectTemplate});
|
||||
|
||||
auto caGrid = Grid{};
|
||||
caGrid.addRow({caProvider, caSelectProvider});
|
||||
caGrid.addRow({caUrl, caSetUrl});
|
||||
caGrid.addRow({caCustomEndpoint, caEndpointMode});
|
||||
caGrid.addRow({caModel, caSelectModel});
|
||||
caGrid.addRow({caTemplate, caSelectTemplate});
|
||||
|
||||
@ -215,7 +254,6 @@ GeneralSettings::GeneralSettings()
|
||||
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
|
||||
Row{enableLogging, Stretch{1}},
|
||||
Row{enableCheckUpdate, Stretch{1}},
|
||||
Row{enableChat, Stretch{1}},
|
||||
Space{8},
|
||||
ccGroup,
|
||||
Space{8},
|
||||
@ -389,6 +427,8 @@ void GeneralSettings::updatePreset1Visiblity(bool state)
|
||||
ccPreset1SelectModel.updateVisibility(specifyPreset1.volatileValue());
|
||||
ccPreset1Template.setVisible(specifyPreset1.volatileValue());
|
||||
ccPreset1SelectTemplate.updateVisibility(specifyPreset1.volatileValue());
|
||||
ccPreset1EndpointMode.setVisible(specifyPreset1.volatileValue());
|
||||
ccPreset1CustomEndpoint.setVisible(specifyPreset1.volatileValue());
|
||||
}
|
||||
|
||||
void GeneralSettings::setupConnections()
|
||||
@ -404,6 +444,19 @@ void GeneralSettings::setupConnections()
|
||||
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
updatePreset1Visiblity(specifyPreset1.volatileValue());
|
||||
});
|
||||
connect(&ccEndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
ccCustomEndpoint.setEnabled(
|
||||
ccEndpointMode.volatileValue() == ccEndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
connect(&ccPreset1EndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
ccPreset1CustomEndpoint.setEnabled(
|
||||
ccPreset1EndpointMode.volatileValue()
|
||||
== ccPreset1EndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
connect(&caEndpointMode, &Utils::BaseAspect::volatileValueChanged, this, [this]() {
|
||||
caCustomEndpoint.setEnabled(
|
||||
caEndpointMode.volatileValue() == caEndpointMode.indexForDisplay("Custom"));
|
||||
});
|
||||
}
|
||||
|
||||
void GeneralSettings::resetPageToDefaults()
|
||||
@ -433,6 +486,12 @@ void GeneralSettings::resetPageToDefaults()
|
||||
resetAspect(ccPreset1Model);
|
||||
resetAspect(ccPreset1Template);
|
||||
resetAspect(ccPreset1Url);
|
||||
resetAspect(ccEndpointMode);
|
||||
resetAspect(ccCustomEndpoint);
|
||||
resetAspect(ccPreset1EndpointMode);
|
||||
resetAspect(ccPreset1CustomEndpoint);
|
||||
resetAspect(caEndpointMode);
|
||||
resetAspect(caCustomEndpoint);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||