Compare commits
8 Commits
v0.9.0
...
dev-rag-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
142afa725f | ||
|
|
f36db033e6 | ||
|
|
5dfcf74128 | ||
|
|
02101665ca | ||
|
|
77a03d42ed | ||
|
|
09c38c8b0e | ||
|
|
7b73d7af7b | ||
|
|
5a426b4d9f |
2
.github/FUNDING.yml
vendored
@@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
|
|||||||
polar: # Replace with a single Polar username
|
polar: # Replace with a single Polar username
|
||||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
thanks_dev: # Replace with a single thanks.dev username
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
custom: ['https://www.paypal.com/paypalme/palm1r', 'https://github.com/Palm1r/QodeAssist#support-the-development-of-qodeassist']
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
|||||||
61
.github/scripts/plugin.json
vendored
@@ -6,77 +6,22 @@
|
|||||||
"llm",
|
"llm",
|
||||||
"ai"
|
"ai"
|
||||||
],
|
],
|
||||||
"compatibility": "Qt 6.8.3",
|
"compatibility": "Qt 6.8.1",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"Windows",
|
"Windows",
|
||||||
"macOS",
|
"macOS",
|
||||||
"Linux"
|
"Linux"
|
||||||
],
|
],
|
||||||
"license": "GPLv3",
|
"license": "GPLv3",
|
||||||
"version": "0.5.11",
|
"version": "0.4.0",
|
||||||
"status": "draft",
|
"status": "draft",
|
||||||
"is_pack": false,
|
"is_pack": false,
|
||||||
"released_at": null,
|
"released_at": null,
|
||||||
"version_history": [
|
"version_history": [
|
||||||
{
|
{
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2024-01-24T15:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.2",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-03-13T17:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.3",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-03-14T11:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.4",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-03-17T03:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.5",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-03-20T19:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.6",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-04-04T19:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.7",
|
|
||||||
"is_latest": false,
|
|
||||||
"released_at": "2025-04-14T01:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "0.5.8",
|
|
||||||
"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,
|
"is_latest": true,
|
||||||
"released_at": "2025-05-01T17:00:00Z"
|
"released_at": "2024-01-24T15:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
||||||
|
|||||||
151
.github/workflows/build_cmake.yml
vendored
@@ -12,13 +12,16 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_NAME: QodeAssist
|
PLUGIN_NAME: QodeAssist
|
||||||
|
QT_VERSION: 6.8.1
|
||||||
|
QT_CREATOR_VERSION: 15.0.1
|
||||||
|
QT_CREATOR_VERSION_INTERNAL: 15.0.1
|
||||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||||
CMAKE_VERSION: "3.29.6"
|
CMAKE_VERSION: "3.29.6"
|
||||||
NINJA_VERSION: "1.12.1"
|
NINJA_VERSION: "1.12.1"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
name: ${{ matrix.config.name }}
|
||||||
runs-on: ${{ matrix.config.os }}
|
runs-on: ${{ matrix.config.os }}
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.git.outputs.tag }}
|
tag: ${{ steps.git.outputs.tag }}
|
||||||
@@ -33,8 +36,8 @@ jobs:
|
|||||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
|
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
||||||
os: ubuntu-22.04,
|
os: ubuntu-latest,
|
||||||
platform: linux_x64,
|
platform: linux_x64,
|
||||||
cc: "gcc", cxx: "g++"
|
cc: "gcc", cxx: "g++"
|
||||||
}
|
}
|
||||||
@@ -44,22 +47,12 @@ jobs:
|
|||||||
platform: mac_x64,
|
platform: mac_x64,
|
||||||
cc: "clang", cxx: "clang++"
|
cc: "clang", cxx: "clang++"
|
||||||
}
|
}
|
||||||
qt_config:
|
|
||||||
- {
|
|
||||||
qt_version: "6.8.3",
|
|
||||||
qt_creator_version: "16.0.2"
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
qt_version: "6.9.2",
|
|
||||||
qt_creator_version: "17.0.2"
|
|
||||||
}
|
|
||||||
- {
|
|
||||||
qt_version: "6.10.0",
|
|
||||||
qt_creator_version: "18.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
id: git
|
id: git
|
||||||
@@ -68,21 +61,16 @@ jobs:
|
|||||||
if (${{github.ref}} MATCHES "tags/v(.*)")
|
if (${{github.ref}} MATCHES "tags/v(.*)")
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||||
else()
|
else()
|
||||||
execute_process(
|
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
||||||
COMMAND git rev-parse --short HEAD
|
|
||||||
OUTPUT_VARIABLE short_sha
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
||||||
)
|
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Download Ninja and CMake
|
- name: Download Ninja and CMake
|
||||||
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
|
uses: lukka/get-cmake@latest
|
||||||
with:
|
with:
|
||||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install system libs
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
run: |
|
run: |
|
||||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||||
@@ -90,13 +78,7 @@ jobs:
|
|||||||
COMMAND sudo apt update
|
COMMAND sudo apt update
|
||||||
)
|
)
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND sudo apt install
|
COMMAND sudo apt install libgl1-mesa-dev
|
||||||
# build dependencies
|
|
||||||
libgl1-mesa-dev libgtest-dev libgmock-dev
|
|
||||||
# runtime dependencies for tests (Qt is downloaded outside package manager,
|
|
||||||
# thus minimal dependencies must be installed explicitly)
|
|
||||||
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
|
|
||||||
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
|
|
||||||
RESULT_VARIABLE result
|
RESULT_VARIABLE result
|
||||||
)
|
)
|
||||||
if (NOT result EQUAL 0)
|
if (NOT result EQUAL 0)
|
||||||
@@ -108,19 +90,14 @@ jobs:
|
|||||||
id: qt
|
id: qt
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
run: |
|
run: |
|
||||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
set(qt_version "$ENV{QT_VERSION}")
|
||||||
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
|
||||||
|
|
||||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||||
set(url_os "windows_x86")
|
set(url_os "windows_x86")
|
||||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
||||||
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
|
|
||||||
else()
|
|
||||||
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
|
|
||||||
endif()
|
|
||||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||||
set(url_os "linux_x64")
|
set(url_os "linux_x64")
|
||||||
if (qt_version VERSION_LESS "6.7.0")
|
if (qt_version VERSION_LESS "6.7.0")
|
||||||
@@ -129,20 +106,12 @@ jobs:
|
|||||||
set(qt_package_arch_suffix "linux_gcc_64")
|
set(qt_package_arch_suffix "linux_gcc_64")
|
||||||
endif()
|
endif()
|
||||||
set(qt_dir_prefix "${qt_version}/gcc_64")
|
set(qt_dir_prefix "${qt_version}/gcc_64")
|
||||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
||||||
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
|
|
||||||
else()
|
|
||||||
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
|
||||||
endif()
|
|
||||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||||
set(url_os "mac_x64")
|
set(url_os "mac_x64")
|
||||||
set(qt_package_arch_suffix "clang_64")
|
set(qt_package_arch_suffix "clang_64")
|
||||||
set(qt_dir_prefix "${qt_version}/macos")
|
set(qt_dir_prefix "${qt_version}/macos")
|
||||||
if (qt_version VERSION_LESS "6.9.1")
|
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||||
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()
|
endif()
|
||||||
|
|
||||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
||||||
@@ -165,7 +134,7 @@ jobs:
|
|||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
foreach(package qtbase qtdeclarative qttools)
|
foreach(package qtbase qtdeclarative)
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||||
${package}.7z
|
${package}.7z
|
||||||
@@ -199,11 +168,10 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Download Qt Creator
|
- name: Download Qt Creator
|
||||||
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
|
uses: qt-creator/install-dev-package@v1.2
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_config.qt_creator_version }}
|
version: ${{ env.QT_CREATOR_VERSION }}
|
||||||
unzip-to: 'qtcreator'
|
unzip-to: 'qtcreator'
|
||||||
platform: ${{ matrix.config.platform }}
|
|
||||||
|
|
||||||
- name: Extract Qt Creator
|
- name: Extract Qt Creator
|
||||||
id: qt_creator
|
id: qt_creator
|
||||||
@@ -249,7 +217,7 @@ jobs:
|
|||||||
COMMAND python
|
COMMAND python
|
||||||
-u
|
-u
|
||||||
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
|
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
|
||||||
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}"
|
--name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
||||||
--src .
|
--src .
|
||||||
--build build
|
--build build
|
||||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||||
@@ -265,24 +233,67 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
path: ./${{ env.PLUGIN_NAME }}-${{ env.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: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||||
|
|
||||||
- name: Run unit tests
|
# The json is the same for all platforms, but we need to save one
|
||||||
if: startsWith(matrix.config.os, 'ubuntu')
|
- name: Upload plugin json
|
||||||
|
if: matrix.config.os == 'ubuntu-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ env.PLUGIN_NAME }}-origin-json
|
||||||
|
path: ./build/build/${{ env.PLUGIN_NAME }}.json
|
||||||
|
|
||||||
|
update_json:
|
||||||
|
if: contains(github.ref, 'tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- 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: |
|
run: |
|
||||||
xvfb-run ./build/build/test/QodeAssistTest
|
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:
|
release:
|
||||||
if: contains(github.ref, 'tags/v')
|
if: contains(github.ref, 'tags/v')
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [build]
|
needs: [build, update_json]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: release-with-dirs
|
path: release-with-dirs
|
||||||
|
|
||||||
@@ -291,21 +302,9 @@ jobs:
|
|||||||
mkdir release
|
mkdir release
|
||||||
mv release-with-dirs/*/* release/
|
mv release-with-dirs/*/* release/
|
||||||
|
|
||||||
- name: Download QodeAssistUpdater
|
|
||||||
run: |
|
|
||||||
# Get latest release info and download assets
|
|
||||||
LATEST_RELEASE=$(curl -s https://api.github.com/repos/Palm1r/QodeAssistUpdater/releases/latest)
|
|
||||||
|
|
||||||
# Download all assets except .sha256 files
|
|
||||||
echo "$LATEST_RELEASE" | jq -r '.assets[].browser_download_url' | grep -v '\.sha256$' | while read url; do
|
|
||||||
filename=$(basename "$url")
|
|
||||||
echo "Downloading $filename..."
|
|
||||||
curl -L -o "release/$filename" "$url"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -73,8 +73,4 @@ CMakeLists.txt.user*
|
|||||||
*.dll
|
*.dll
|
||||||
*.exe
|
*.exe
|
||||||
|
|
||||||
/build
|
/build
|
||||||
/.qodeassist
|
|
||||||
/.cursor
|
|
||||||
/.vscode
|
|
||||||
.qtc_clangd/compile_commands.json
|
|
||||||
0
.gitmodules
vendored
109
CMakeLists.txt
@@ -8,41 +8,15 @@ set(CMAKE_AUTOUIC ON)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
||||||
|
|
||||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network 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})
|
|
||||||
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
|
|
||||||
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
|
|
||||||
|
|
||||||
if(NOT version_match)
|
|
||||||
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
|
|
||||||
|
|
||||||
add_definitions(
|
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
|
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
|
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
|
||||||
)
|
|
||||||
|
|
||||||
add_subdirectory(llmcore)
|
add_subdirectory(llmcore)
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
add_subdirectory(UIControls)
|
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
add_subdirectory(context)
|
add_subdirectory(context)
|
||||||
if(GTest_FOUND)
|
|
||||||
add_subdirectory(test)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_qtc_plugin(QodeAssist
|
add_qtc_plugin(QodeAssist
|
||||||
PLUGIN_DEPENDS
|
PLUGIN_DEPENDS
|
||||||
@@ -50,7 +24,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QtCreator::LanguageClient
|
QtCreator::LanguageClient
|
||||||
QtCreator::TextEditor
|
QtCreator::TextEditor
|
||||||
QtCreator::ProjectExplorer
|
QtCreator::ProjectExplorer
|
||||||
QtCreator::CppEditor
|
|
||||||
DEPENDS
|
DEPENDS
|
||||||
Qt::Core
|
Qt::Core
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
@@ -59,7 +32,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::ExtensionSystem
|
QtCreator::ExtensionSystem
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
|
||||||
QodeAssistChatViewplugin
|
QodeAssistChatViewplugin
|
||||||
SOURCES
|
SOURCES
|
||||||
.github/workflows/build_cmake.yml
|
.github/workflows/build_cmake.yml
|
||||||
@@ -71,94 +43,33 @@ add_qtc_plugin(QodeAssist
|
|||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
templates/Templates.hpp
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/Ollama.hpp
|
|
||||||
templates/Claude.hpp
|
|
||||||
templates/OpenAI.hpp
|
|
||||||
templates/MistralAI.hpp
|
|
||||||
templates/StarCoder2Fim.hpp
|
templates/StarCoder2Fim.hpp
|
||||||
# templates/DeepSeekCoderFim.hpp
|
templates/DeepSeekCoderFim.hpp
|
||||||
# templates/CustomFimTemplate.hpp
|
templates/CustomFimTemplate.hpp
|
||||||
templates/Qwen25CoderFIM.hpp
|
templates/Qwen.hpp
|
||||||
templates/OpenAICompatible.hpp
|
templates/Ollama.hpp
|
||||||
|
templates/BasicChat.hpp
|
||||||
templates/Llama3.hpp
|
templates/Llama3.hpp
|
||||||
templates/ChatML.hpp
|
templates/ChatML.hpp
|
||||||
templates/Alpaca.hpp
|
templates/Alpaca.hpp
|
||||||
templates/Llama2.hpp
|
templates/Llama2.hpp
|
||||||
|
templates/Claude.hpp
|
||||||
|
templates/OpenAI.hpp
|
||||||
templates/CodeLlamaQMLFim.hpp
|
templates/CodeLlamaQMLFim.hpp
|
||||||
templates/GoogleAI.hpp
|
|
||||||
templates/LlamaCppFim.hpp
|
|
||||||
templates/Qwen3CoderFIM.hpp
|
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
|
||||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
|
||||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
RefactorSuggestion.hpp RefactorSuggestion.cpp
|
|
||||||
RefactorSuggestionHoverHandler.hpp RefactorSuggestionHoverHandler.cpp
|
|
||||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||||
CodeHandler.hpp CodeHandler.cpp
|
CodeHandler.hpp CodeHandler.cpp
|
||||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
|
||||||
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
|
|
||||||
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
|
|
||||||
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
|
|
||||||
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
|
|
||||||
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
|
|
||||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
|
||||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
|
||||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
|
||||||
|
|
||||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
|
||||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
|
||||||
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
|
||||||
tools/ToolHandler.hpp tools/ToolHandler.cpp
|
|
||||||
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
|
||||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
|
||||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
|
||||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
|
||||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
|
||||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
|
||||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
|
||||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
|
||||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
|
||||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
|
||||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
|
||||||
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
|
||||||
find_program(QtCreatorExecutable
|
|
||||||
NAMES
|
|
||||||
qtcreator "Qt Creator"
|
|
||||||
PATHS
|
|
||||||
"${QtCreatorCorePath}/../../../bin"
|
|
||||||
"${QtCreatorCorePath}/../../../MacOS"
|
|
||||||
NO_DEFAULT_PATH
|
|
||||||
)
|
|
||||||
if (QtCreatorExecutable)
|
|
||||||
add_custom_target(RunQtCreator
|
|
||||||
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
|
|
||||||
DEPENDS QodeAssist
|
|
||||||
)
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,23 +6,17 @@ qt_policy(SET QTP0004 NEW)
|
|||||||
qt_add_qml_module(QodeAssistChatView
|
qt_add_qml_module(QodeAssistChatView
|
||||||
URI ChatView
|
URI ChatView
|
||||||
VERSION 1.0
|
VERSION 1.0
|
||||||
DEPENDENCIES
|
DEPENDENCIES QtQuick
|
||||||
QtQuick
|
|
||||||
QML_FILES
|
QML_FILES
|
||||||
qml/RootItem.qml
|
qml/RootItem.qml
|
||||||
qml/ChatItem.qml
|
qml/ChatItem.qml
|
||||||
|
qml/Badge.qml
|
||||||
qml/dialog/CodeBlock.qml
|
qml/dialog/CodeBlock.qml
|
||||||
qml/dialog/TextBlock.qml
|
qml/dialog/TextBlock.qml
|
||||||
|
qml/controls/QoAButton.qml
|
||||||
qml/parts/TopBar.qml
|
qml/parts/TopBar.qml
|
||||||
qml/parts/BottomBar.qml
|
qml/parts/BottomBar.qml
|
||||||
qml/parts/AttachedFilesPlace.qml
|
qml/parts/AttachedFilesPlace.qml
|
||||||
qml/parts/Toast.qml
|
|
||||||
qml/ToolStatusItem.qml
|
|
||||||
qml/ThinkingStatusItem.qml
|
|
||||||
qml/FileEditItem.qml
|
|
||||||
qml/parts/RulesViewer.qml
|
|
||||||
qml/parts/FileEditsActionBar.qml
|
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/attach-file-light.svg
|
icons/attach-file-light.svg
|
||||||
icons/attach-file-dark.svg
|
icons/attach-file-dark.svg
|
||||||
@@ -30,22 +24,6 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/close-light.svg
|
icons/close-light.svg
|
||||||
icons/link-file-light.svg
|
icons/link-file-light.svg
|
||||||
icons/link-file-dark.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
|
|
||||||
icons/rules-icon.svg
|
|
||||||
icons/open-in-editor.svg
|
|
||||||
icons/apply-changes-button.svg
|
|
||||||
icons/undo-changes-button.svg
|
|
||||||
icons/reject-changes-button.svg
|
|
||||||
icons/thinking-icon-on.svg
|
|
||||||
icons/thinking-icon-off.svg
|
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
ChatModel.hpp ChatModel.cpp
|
ChatModel.hpp ChatModel.cpp
|
||||||
@@ -54,9 +32,6 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
MessagePart.hpp
|
MessagePart.hpp
|
||||||
ChatUtils.h ChatUtils.cpp
|
ChatUtils.h ChatUtils.cpp
|
||||||
ChatSerializer.hpp ChatSerializer.cpp
|
ChatSerializer.hpp ChatSerializer.cpp
|
||||||
ChatView.hpp ChatView.cpp
|
|
||||||
ChatData.hpp
|
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -70,8 +45,6 @@ target_link_libraries(QodeAssistChatView
|
|||||||
LLMCore
|
LLMCore
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
Context
|
Context
|
||||||
QodeAssistUIControlsplugin
|
|
||||||
QodeAssistLogger
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <QtQmlIntegration>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
Q_NAMESPACE
|
|
||||||
QML_NAMED_ELEMENT(MessagePartType)
|
|
||||||
|
|
||||||
enum class MessagePartType { Code, Text };
|
|
||||||
Q_ENUM_NS(MessagePartType)
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -18,15 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include <utils/aspects.h>
|
#include <QtCore/qjsonobject.h>
|
||||||
#include <QDateTime>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "Logger.hpp"
|
|
||||||
#include "context/ChangesManager.h"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -35,26 +31,10 @@ ChatModel::ChatModel(QObject *parent)
|
|||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
connect(
|
connect(&settings.chatTokensThreshold,
|
||||||
&settings.chatTokensThreshold,
|
&Utils::BaseAspect::changed,
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatModel::tokensThresholdChanged);
|
|
||||||
|
|
||||||
connect(&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditApplied,
|
|
||||||
this,
|
this,
|
||||||
&ChatModel::onFileEditApplied);
|
&ChatModel::tokensThresholdChanged);
|
||||||
|
|
||||||
connect(&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditRejected,
|
|
||||||
this,
|
|
||||||
&ChatModel::onFileEditRejected);
|
|
||||||
|
|
||||||
connect(&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditArchived,
|
|
||||||
this,
|
|
||||||
&ChatModel::onFileEditArchived);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||||
@@ -81,9 +61,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
return filenames;
|
return filenames;
|
||||||
}
|
}
|
||||||
case Roles::IsRedacted: {
|
|
||||||
return message.isRedacted;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -95,7 +72,6 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
|||||||
roles[Roles::RoleType] = "roleType";
|
roles[Roles::RoleType] = "roleType";
|
||||||
roles[Roles::Content] = "content";
|
roles[Roles::Content] = "content";
|
||||||
roles[Roles::Attachments] = "attachments";
|
roles[Roles::Attachments] = "attachments";
|
||||||
roles[Roles::IsRedacted] = "isRedacted";
|
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +90,7 @@ void ChatModel::addMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
|
||||||
&& m_messages.last().role == role) {
|
|
||||||
Message &lastMessage = m_messages.last();
|
Message &lastMessage = m_messages.last();
|
||||||
lastMessage.content = content;
|
lastMessage.content = content;
|
||||||
lastMessage.attachments = attachments;
|
lastMessage.attachments = attachments;
|
||||||
@@ -126,45 +101,6 @@ void ChatModel::addMessage(
|
|||||||
newMessage.attachments = attachments;
|
newMessage.attachments = attachments;
|
||||||
m_messages.append(newMessage);
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
if (m_loadingFromHistory && role == ChatRole::FileEdit) {
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
if (content.contains(marker)) {
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
int jsonStart = markerPos + marker.length();
|
|
||||||
|
|
||||||
if (jsonStart < content.length()) {
|
|
||||||
QString jsonStr = content.mid(jsonStart);
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject editData = doc.object();
|
|
||||||
QString editId = editData.value("edit_id").toString();
|
|
||||||
QString filePath = editData.value("file").toString();
|
|
||||||
QString oldContent = editData.value("old_content").toString();
|
|
||||||
QString newContent = editData.value("new_content").toString();
|
|
||||||
QString originalStatus = editData.value("status").toString();
|
|
||||||
|
|
||||||
if (!editId.isEmpty() && !filePath.isEmpty()) {
|
|
||||||
Context::ChangesManager::instance().addFileEdit(
|
|
||||||
editId, filePath, oldContent, newContent, false, true);
|
|
||||||
|
|
||||||
editData["status"] = "archived";
|
|
||||||
editData["status_message"] = "Loaded from chat history";
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
|
||||||
m_messages.last().content = updatedContent;
|
|
||||||
|
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Registered historical file edit: %1 (original status: %2, now: archived)")
|
|
||||||
.arg(editId, originalStatus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,50 +127,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
|||||||
while (blockMatches.hasNext()) {
|
while (blockMatches.hasNext()) {
|
||||||
auto match = blockMatches.next();
|
auto match = blockMatches.next();
|
||||||
if (match.capturedStart() > lastIndex) {
|
if (match.capturedStart() > lastIndex) {
|
||||||
QString textBetween
|
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||||
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
|
||||||
if (!textBetween.isEmpty()) {
|
if (!textBetween.isEmpty()) {
|
||||||
MessagePart part;
|
parts.append({MessagePart::Text, textBetween, ""});
|
||||||
part.type = MessagePartType::Text;
|
|
||||||
part.text = textBetween;
|
|
||||||
parts.append(part);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
|
||||||
MessagePart codePart;
|
|
||||||
codePart.type = MessagePartType::Code;
|
|
||||||
codePart.text = match.captured(2).trimmed();
|
|
||||||
codePart.language = match.captured(1);
|
|
||||||
parts.append(codePart);
|
|
||||||
|
|
||||||
lastIndex = match.capturedEnd();
|
lastIndex = match.capturedEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < content.length()) {
|
if (lastIndex < content.length()) {
|
||||||
QString remainingText = content.mid(lastIndex).trimmed();
|
QString remainingText = content.mid(lastIndex).trimmed();
|
||||||
|
if (!remainingText.isEmpty()) {
|
||||||
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
parts.append({MessagePart::Text, remainingText, ""});
|
||||||
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
|
|
||||||
|
|
||||||
if (unclosedMatch.hasMatch()) {
|
|
||||||
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
|
|
||||||
if (!beforeCodeBlock.isEmpty()) {
|
|
||||||
MessagePart part;
|
|
||||||
part.type = MessagePartType::Text;
|
|
||||||
part.text = beforeCodeBlock;
|
|
||||||
parts.append(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
MessagePart codePart;
|
|
||||||
codePart.type = MessagePartType::Code;
|
|
||||||
codePart.text = unclosedMatch.captured(2).trimmed();
|
|
||||||
codePart.language = unclosedMatch.captured(1);
|
|
||||||
parts.append(codePart);
|
|
||||||
} else if (!remainingText.isEmpty()) {
|
|
||||||
MessagePart part;
|
|
||||||
part.type = MessagePartType::Text;
|
|
||||||
part.text = remainingText;
|
|
||||||
parts.append(part);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,9 +160,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
|
|||||||
case ChatRole::Assistant:
|
case ChatRole::Assistant:
|
||||||
role = "assistant";
|
role = "assistant";
|
||||||
break;
|
break;
|
||||||
case ChatRole::Tool:
|
|
||||||
case ChatRole::FileEdit:
|
|
||||||
continue;
|
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -293,245 +195,4 @@ QString ChatModel::lastMessageId() const
|
|||||||
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatModel::resetModelTo(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_messages.size())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (index < m_messages.size()) {
|
|
||||||
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
|
||||||
m_messages.remove(index, m_messages.size() - index);
|
|
||||||
endRemoveRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::addToolExecutionStatus(
|
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
|
||||||
{
|
|
||||||
QString content = toolName;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Adding tool execution status: requestId=%1, toolId=%2, toolName=%3")
|
|
||||||
.arg(requestId, toolId, toolName));
|
|
||||||
|
|
||||||
if (!m_messages.isEmpty() && !toolId.isEmpty() && m_messages.last().id == toolId
|
|
||||||
&& m_messages.last().role == ChatRole::Tool) {
|
|
||||||
Message &lastMessage = m_messages.last();
|
|
||||||
lastMessage.content = content;
|
|
||||||
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
|
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
|
||||||
} else {
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
|
||||||
Message newMessage{ChatRole::Tool, content, toolId};
|
|
||||||
m_messages.append(newMessage);
|
|
||||||
endInsertRows();
|
|
||||||
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
|
|
||||||
.arg(m_messages.size() - 1)
|
|
||||||
.arg(toolId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::updateToolResult(
|
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
|
|
||||||
{
|
|
||||||
if (m_messages.isEmpty() || toolId.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("Cannot update tool result: messages empty=%1, toolId empty=%2")
|
|
||||||
.arg(m_messages.isEmpty())
|
|
||||||
.arg(toolId.isEmpty()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Updating tool result: requestId=%1, toolId=%2, toolName=%3, result length=%4")
|
|
||||||
.arg(requestId, toolId, toolName)
|
|
||||||
.arg(result.length()));
|
|
||||||
|
|
||||||
bool toolMessageFound = false;
|
|
||||||
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
|
||||||
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
|
|
||||||
m_messages[i].content = toolName + "\n" + result;
|
|
||||||
emit dataChanged(index(i), index(i));
|
|
||||||
toolMessageFound = true;
|
|
||||||
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!toolMessageFound) {
|
|
||||||
LOG_MESSAGE(QString("WARNING: Tool message with requestId=%1 toolId=%2 not found!")
|
|
||||||
.arg(requestId, toolId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
if (result.contains(marker)) {
|
|
||||||
LOG_MESSAGE(QString("File edit marker detected in tool result"));
|
|
||||||
|
|
||||||
int markerPos = result.indexOf(marker);
|
|
||||||
int jsonStart = markerPos + marker.length();
|
|
||||||
|
|
||||||
if (jsonStart < result.length()) {
|
|
||||||
QString jsonStr = result.mid(jsonStart);
|
|
||||||
|
|
||||||
QJsonParseError parseError;
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
|
|
||||||
|
|
||||||
if (parseError.error != QJsonParseError::NoError) {
|
|
||||||
LOG_MESSAGE(QString("ERROR: Failed to parse file edit JSON at offset %1: %2")
|
|
||||||
.arg(parseError.offset)
|
|
||||||
.arg(parseError.errorString()));
|
|
||||||
} else if (!doc.isObject()) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("ERROR: Parsed JSON is not an object, is array=%1").arg(doc.isArray()));
|
|
||||||
} else {
|
|
||||||
QJsonObject editData = doc.object();
|
|
||||||
|
|
||||||
QString editId = editData.value("edit_id").toString();
|
|
||||||
|
|
||||||
if (editId.isEmpty()) {
|
|
||||||
editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
|
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
|
||||||
Message fileEditMsg;
|
|
||||||
fileEditMsg.role = ChatRole::FileEdit;
|
|
||||||
fileEditMsg.content = result;
|
|
||||||
fileEditMsg.id = editId;
|
|
||||||
m_messages.append(fileEditMsg);
|
|
||||||
endInsertRows();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Added FileEdit message with editId=%1").arg(editId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::addThinkingBlock(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Adding thinking block: requestId=%1, thinking length=%2, signature length=%3")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(thinking.length())
|
|
||||||
.arg(signature.length()));
|
|
||||||
|
|
||||||
QString displayContent = thinking;
|
|
||||||
if (!signature.isEmpty()) {
|
|
||||||
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
|
||||||
}
|
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
|
||||||
Message thinkingMessage;
|
|
||||||
thinkingMessage.role = ChatRole::Thinking;
|
|
||||||
thinkingMessage.content = displayContent;
|
|
||||||
thinkingMessage.id = requestId;
|
|
||||||
thinkingMessage.isRedacted = false;
|
|
||||||
thinkingMessage.signature = signature;
|
|
||||||
m_messages.append(thinkingMessage);
|
|
||||||
endInsertRows();
|
|
||||||
LOG_MESSAGE(QString("Added thinking message at index %1 with signature length=%2")
|
|
||||||
.arg(m_messages.size() - 1).arg(signature.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::addRedactedThinkingBlock(const QString &requestId, const QString &signature)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Adding redacted thinking block: requestId=%1, signature length=%2")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(signature.length()));
|
|
||||||
|
|
||||||
QString displayContent = "[Thinking content redacted by safety systems]";
|
|
||||||
if (!signature.isEmpty()) {
|
|
||||||
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
|
||||||
}
|
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
|
||||||
Message thinkingMessage;
|
|
||||||
thinkingMessage.role = ChatRole::Thinking;
|
|
||||||
thinkingMessage.content = displayContent;
|
|
||||||
thinkingMessage.id = requestId;
|
|
||||||
thinkingMessage.isRedacted = true;
|
|
||||||
thinkingMessage.signature = signature;
|
|
||||||
m_messages.append(thinkingMessage);
|
|
||||||
endInsertRows();
|
|
||||||
LOG_MESSAGE(QString("Added redacted thinking message at index %1 with signature length=%2")
|
|
||||||
.arg(m_messages.size() - 1).arg(signature.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
|
||||||
if (m_messages[i].id == messageId) {
|
|
||||||
m_messages[i].content = newContent;
|
|
||||||
emit dataChanged(index(i), index(i));
|
|
||||||
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::setLoadingFromHistory(bool loading)
|
|
||||||
{
|
|
||||||
m_loadingFromHistory = loading;
|
|
||||||
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatModel::isLoadingFromHistory() const
|
|
||||||
{
|
|
||||||
return m_loadingFromHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::onFileEditApplied(const QString &editId)
|
|
||||||
{
|
|
||||||
updateFileEditStatus(editId, "applied", "Successfully applied");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::onFileEditRejected(const QString &editId)
|
|
||||||
{
|
|
||||||
updateFileEditStatus(editId, "rejected", "Rejected by user");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::onFileEditArchived(const QString &editId)
|
|
||||||
{
|
|
||||||
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
|
|
||||||
{
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
|
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
|
||||||
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
|
|
||||||
const QString &content = m_messages[i].content;
|
|
||||||
|
|
||||||
if (content.contains(marker)) {
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
int jsonStart = markerPos + marker.length();
|
|
||||||
|
|
||||||
if (jsonStart < content.length()) {
|
|
||||||
QString jsonStr = content.mid(jsonStart);
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject editData = doc.object();
|
|
||||||
|
|
||||||
editData["status"] = status;
|
|
||||||
editData["status_message"] = statusMessage;
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
|
||||||
|
|
||||||
m_messages[i].content = updatedContent;
|
|
||||||
|
|
||||||
emit dataChanged(index(i), index(i));
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
|
|
||||||
.arg(editId, status));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -37,19 +37,16 @@ class ChatModel : public QAbstractListModel
|
|||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
enum ChatRole { System, User, Assistant };
|
||||||
Q_ENUM(ChatRole)
|
Q_ENUM(ChatRole)
|
||||||
|
|
||||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted };
|
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
||||||
Q_ENUM(Roles)
|
|
||||||
|
|
||||||
struct Message
|
struct Message
|
||||||
{
|
{
|
||||||
ChatRole role;
|
ChatRole role;
|
||||||
QString content;
|
QString content;
|
||||||
QString id;
|
QString id;
|
||||||
bool isRedacted = false;
|
|
||||||
QString signature = QString();
|
|
||||||
|
|
||||||
QList<Context::ContentFile> attachments;
|
QList<Context::ContentFile> attachments;
|
||||||
};
|
};
|
||||||
@@ -76,37 +73,12 @@ public:
|
|||||||
QString currentModel() const;
|
QString currentModel() const;
|
||||||
QString lastMessageId() const;
|
QString lastMessageId() const;
|
||||||
|
|
||||||
Q_INVOKABLE void resetModelTo(int index);
|
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
|
||||||
void updateToolResult(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &result);
|
|
||||||
void addThinkingBlock(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
|
||||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
|
||||||
|
|
||||||
void setLoadingFromHistory(bool loading);
|
|
||||||
bool isLoadingFromHistory() const;
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void tokensThresholdChanged();
|
void tokensThresholdChanged();
|
||||||
void modelReseted();
|
void modelReseted();
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onFileEditApplied(const QString &editId);
|
|
||||||
void onFileEditRejected(const QString &editId);
|
|
||||||
void onFileEditArchived(const QString &editId);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
|
||||||
|
|
||||||
QVector<Message> m_messages;
|
QVector<Message> m_messages;
|
||||||
bool m_loadingFromHistory = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -29,43 +29,40 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectexplorer.h>
|
#include <projectexplorer/projectexplorer.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <projectexplorer/projecttree.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
#include "context/ChangesManager.h"
|
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
|
#include "context/FileChunker.hpp"
|
||||||
|
#include "context/RAGManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
#include "llmcore/RulesLoader.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
|
||||||
, m_isRequestInProgress(false)
|
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
|
||||||
&Settings::chatAssistantSettings().linkOpenFiles,
|
this,
|
||||||
&Utils::BaseAspect::changed,
|
[this](){
|
||||||
this,
|
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
|
||||||
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
|
});
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
|
|
||||||
connect(
|
connect(&settings.caModel,
|
||||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::currentTemplateChanged);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
@@ -73,33 +70,19 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::autosave);
|
&ChatRootView::autosave);
|
||||||
|
|
||||||
connect(m_clientInterface, &ClientInterface::messageReceivedCompletely, this, [this]() {
|
|
||||||
this->setRequestProgressStatus(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageReceivedCompletely,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::updateInputTokensCount);
|
&ChatRootView::updateInputTokensCount);
|
||||||
|
|
||||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
|
||||||
setRecentFilePath(QString{});
|
|
||||||
m_currentMessageRequestId.clear();
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
});
|
|
||||||
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||||
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||||
connect(
|
connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
|
||||||
&Settings::chatAssistantSettings().useSystemPrompt,
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
&Utils::BaseAspect::changed,
|
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
|
||||||
this,
|
this, &ChatRootView::updateInputTokensCount);
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().systemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
|
|
||||||
auto editors = Core::EditorManager::instance();
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
@@ -117,106 +100,8 @@ 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);
|
|
||||||
connect(m_clientInterface, &ClientInterface::errorOccurred, this, [this](const QString &error) {
|
|
||||||
this->setRequestProgressStatus(false);
|
|
||||||
m_lastErrorMessage = error;
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
|
||||||
if (!m_currentMessageRequestId.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentMessageRequestId = requestId;
|
|
||||||
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditAdded,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditApplied,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditRejected,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditUndone,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditArchived,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
refreshRules();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
|
||||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::refreshRules);
|
|
||||||
|
|
||||||
QSettings appSettings;
|
|
||||||
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
|
|
||||||
m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().enableThinkingMode,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
[this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::toolsSettings().useTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::toolsSupportEnabledChanged);
|
|
||||||
connect(
|
|
||||||
&Settings::generalSettings().caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::isThinkingSupportChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@@ -242,9 +127,8 @@ void ChatRootView::sendMessage(const QString &message)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode);
|
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
setRequestProgressStatus(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::copyToClipboard(const QString &text)
|
void ChatRootView::copyToClipboard(const QString &text)
|
||||||
@@ -255,7 +139,6 @@ void ChatRootView::copyToClipboard(const QString &text)
|
|||||||
void ChatRootView::cancelRequest()
|
void ChatRootView::cancelRequest()
|
||||||
{
|
{
|
||||||
m_clientInterface->cancelRequest();
|
m_clientInterface->cancelRequest();
|
||||||
setRequestProgressStatus(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::clearAttachmentFiles()
|
void ChatRootView::clearAttachmentFiles()
|
||||||
@@ -280,10 +163,9 @@ QString ChatRootView::getChatsHistoryDir() const
|
|||||||
|
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@@ -319,10 +201,7 @@ void ChatRootView::loadHistory(const QString &filePath)
|
|||||||
} else {
|
} else {
|
||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentMessageRequestId.clear();
|
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showSaveDialog()
|
void ChatRootView::showSaveDialog()
|
||||||
@@ -420,7 +299,8 @@ QString ChatRootView::getSuggestedFileName() const
|
|||||||
QFileInfo finalCheck(fullPath);
|
QFileInfo finalCheck(fullPath);
|
||||||
|
|
||||||
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||||
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
fileName = QString("chat_%1").arg(
|
||||||
|
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileName;
|
return fileName;
|
||||||
@@ -469,7 +349,7 @@ void ChatRootView::showAttachFilesDialog()
|
|||||||
dialog.setFileMode(QFileDialog::ExistingFiles);
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||||
|
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
dialog.setDirectory(project->projectDirectory().toFSPathString());
|
dialog.setDirectory(project->projectDirectory().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
@@ -503,7 +383,7 @@ void ChatRootView::showLinkFilesDialog()
|
|||||||
dialog.setFileMode(QFileDialog::ExistingFiles);
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
||||||
|
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
dialog.setDirectory(project->projectDirectory().toFSPathString());
|
dialog.setDirectory(project->projectDirectory().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
@@ -556,10 +436,9 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
QString path;
|
QString path;
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@@ -571,46 +450,90 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::openRulesFolder()
|
// ChatRootView.cpp
|
||||||
|
|
||||||
|
void ChatRootView::testRAG(const QString &message)
|
||||||
{
|
{
|
||||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
auto project = ProjectExplorer::ProjectTree::currentProject();
|
||||||
if (!project) {
|
if (!project) {
|
||||||
|
qDebug() << "No active project found";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString projectPath = project->projectDirectory().toFSPathString();
|
const QString TEST_QUERY = message;
|
||||||
QString rulesPath = projectPath + "/.qodeassist/rules";
|
|
||||||
|
|
||||||
QDir dir(rulesPath);
|
qDebug() << "Starting RAG test with query:";
|
||||||
if (!dir.exists()) {
|
qDebug() << TEST_QUERY;
|
||||||
dir.mkpath(".");
|
qDebug() << "\nFirst, processing project files...";
|
||||||
|
|
||||||
|
auto files = Context::ContextManager::instance().getProjectSourceFiles(project);
|
||||||
|
// Было: auto future = Context::RAGManager::instance().processFiles(project, files);
|
||||||
|
// Стало:
|
||||||
|
auto future = Context::RAGManager::instance().processProjectFiles(project, files);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&Context::RAGManager::instance(),
|
||||||
|
&Context::RAGManager::vectorizationProgress,
|
||||||
|
this,
|
||||||
|
[](int processed, int total) {
|
||||||
|
qDebug() << QString("Vectorization progress: %1 of %2 files").arg(processed).arg(total);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&Context::RAGManager::instance(),
|
||||||
|
&Context::RAGManager::vectorizationFinished,
|
||||||
|
this,
|
||||||
|
[this, project, TEST_QUERY]() {
|
||||||
|
qDebug() << "\nVectorization completed. Starting similarity search...\n";
|
||||||
|
// Было: Context::RAGManager::instance().searchSimilarDocuments(TEST_QUERY, project, 5);
|
||||||
|
// Стало:
|
||||||
|
auto future = Context::RAGManager::instance().findRelevantChunks(TEST_QUERY, project, 5);
|
||||||
|
future.then([](const QList<Context::RAGManager::ChunkSearchResult> &results) {
|
||||||
|
qDebug() << "Found" << results.size() << "relevant chunks:";
|
||||||
|
for (const auto &result : results) {
|
||||||
|
qDebug() << "File:" << result.filePath;
|
||||||
|
qDebug() << "Lines:" << result.startLine << "-" << result.endLine;
|
||||||
|
qDebug() << "Score:" << result.combinedScore;
|
||||||
|
qDebug() << "Content:" << result.content;
|
||||||
|
qDebug() << "---";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::testChunking()
|
||||||
|
{
|
||||||
|
auto project = ProjectExplorer::ProjectTree::currentProject();
|
||||||
|
if (!project) {
|
||||||
|
qDebug() << "No active project found";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
Context::FileChunker::ChunkingConfig config;
|
||||||
QDesktopServices::openUrl(url);
|
Context::ContextManager::instance().testProjectChunks(project, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokensCount;
|
int inputTokens = m_messageTokensCount;
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto& settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
if (settings.useSystemPrompt()) {
|
if (settings.useSystemPrompt()) {
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_attachmentFiles.isEmpty()) {
|
if (!m_attachmentFiles.isEmpty()) {
|
||||||
auto attachFiles = m_clientInterface->contextManager()->getContentFiles(m_attachmentFiles);
|
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
|
||||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_linkedFiles.isEmpty()) {
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
auto linkFiles = m_clientInterface->contextManager()->getContentFiles(m_linkedFiles);
|
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
|
||||||
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &history = m_chatModel->getChatHistory();
|
const auto& history = m_chatModel->getChatHistory();
|
||||||
for (const auto &message : history) {
|
for (const auto& message : history) {
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||||
inputTokens += 4; // + role
|
inputTokens += 4; // + role
|
||||||
}
|
}
|
||||||
@@ -632,7 +555,7 @@ bool ChatRootView::isSyncOpenFiles() const
|
|||||||
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
||||||
{
|
{
|
||||||
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
QString filePath = document->filePath().toFSPathString();
|
QString filePath = document->filePath().toString();
|
||||||
m_linkedFiles.removeOne(filePath);
|
m_linkedFiles.removeOne(filePath);
|
||||||
emit linkedFilesChanged();
|
emit linkedFilesChanged();
|
||||||
}
|
}
|
||||||
@@ -645,8 +568,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
|||||||
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
||||||
{
|
{
|
||||||
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
||||||
QString filePath = document->filePath().toFSPathString();
|
QString filePath = document->filePath().toString();
|
||||||
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) {
|
if (!m_linkedFiles.contains(filePath)) {
|
||||||
m_linkedFiles.append(filePath);
|
m_linkedFiles.append(filePath);
|
||||||
emit linkedFilesChanged();
|
emit linkedFilesChanged();
|
||||||
}
|
}
|
||||||
@@ -673,458 +596,4 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
|
||||||
{
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
|
||||||
if (project
|
|
||||||
&& m_clientInterface->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(filePath.toFSPathString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
|
||||||
.arg(filePath.toFSPathString()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::lastErrorMessage() const
|
|
||||||
{
|
|
||||||
return m_lastErrorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList ChatRootView::activeRules() const
|
|
||||||
{
|
|
||||||
return m_activeRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::activeRulesCount() const
|
|
||||||
{
|
|
||||||
return m_activeRules.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::getRuleContent(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_activeRules.size())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return LLMCore::RulesLoader::loadRuleFileContent(
|
|
||||||
m_activeRules[index].toMap()["filePath"].toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::refreshRules()
|
|
||||||
{
|
|
||||||
m_activeRules.clear();
|
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (!project) {
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ruleFiles
|
|
||||||
= LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
for (const auto &ruleFile : ruleFiles) {
|
|
||||||
QVariantMap ruleMap;
|
|
||||||
ruleMap["filePath"] = ruleFile.filePath;
|
|
||||||
ruleMap["fileName"] = ruleFile.fileName;
|
|
||||||
ruleMap["category"] = ruleFile.category;
|
|
||||||
m_activeRules.append(ruleMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::isAgentMode() const
|
|
||||||
{
|
|
||||||
return m_isAgentMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setIsAgentMode(bool newIsAgentMode)
|
|
||||||
{
|
|
||||||
if (m_isAgentMode != newIsAgentMode) {
|
|
||||||
m_isAgentMode = newIsAgentMode;
|
|
||||||
|
|
||||||
QSettings settings;
|
|
||||||
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
|
|
||||||
|
|
||||||
emit isAgentModeChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::isThinkingMode() const
|
|
||||||
{
|
|
||||||
return m_isThinkingMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setIsThinkingMode(bool newIsThinkingMode)
|
|
||||||
{
|
|
||||||
if (m_isThinkingMode != newIsThinkingMode) {
|
|
||||||
m_isThinkingMode = newIsThinkingMode;
|
|
||||||
|
|
||||||
Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
|
|
||||||
emit isThinkingModeChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::toolsSupportEnabled() const
|
|
||||||
{
|
|
||||||
return Settings::toolsSettings().useTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::applyFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit applied successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to apply file edit")
|
|
||||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::rejectFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit rejected");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to reject file edit")
|
|
||||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::undoFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit undone successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to undo file edit")
|
|
||||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::openFileEditInEditor(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (edit.editId.isEmpty()) {
|
|
||||||
m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
|
||||||
|
|
||||||
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
|
||||||
if (!editor) {
|
|
||||||
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
|
||||||
if (textEditor && textEditor->editorWidget()) {
|
|
||||||
QTextDocument *doc = textEditor->editorWidget()->document();
|
|
||||||
if (doc) {
|
|
||||||
QString currentContent = doc->toPlainText();
|
|
||||||
int position = -1;
|
|
||||||
|
|
||||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
|
||||||
position = currentContent.indexOf(edit.newContent);
|
|
||||||
}
|
|
||||||
else if (!edit.oldContent.isEmpty()) {
|
|
||||||
position = currentContent.indexOf(edit.oldContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position >= 0) {
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
cursor.setPosition(position);
|
|
||||||
textEditor->editorWidget()->setTextCursor(cursor);
|
|
||||||
textEditor->editorWidget()->centerCursor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::updateFileEditStatus(const QString &editId, const QString &status)
|
|
||||||
{
|
|
||||||
auto messages = m_chatModel->getChatHistory();
|
|
||||||
for (int i = 0; i < messages.size(); ++i) {
|
|
||||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
|
||||||
QString content = messages[i].content;
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
|
|
||||||
QString jsonStr = content;
|
|
||||||
if (markerPos >= 0) {
|
|
||||||
jsonStr = content.mid(markerPos + marker.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
obj["status"] = status;
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (!edit.statusMessage.isEmpty()) {
|
|
||||||
obj["status_message"] = edit.statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString updatedContent = marker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
|
||||||
{
|
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
|
||||||
m_lastErrorMessage = QString("No active message with file edits");
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
m_lastInfoMessage = QString("All file edits applied successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
|
||||||
? QString("Failed to apply some file edits")
|
|
||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|
||||||
{
|
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
|
||||||
m_lastErrorMessage = QString("No active message with file edits");
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
m_lastInfoMessage = QString("All file edits undone successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
|
||||||
? QString("Failed to undo some file edits")
|
|
||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::updateCurrentMessageEditsStats()
|
|
||||||
{
|
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
|
||||||
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 ||
|
|
||||||
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
|
|
||||||
m_currentMessageTotalEdits = 0;
|
|
||||||
m_currentMessageAppliedEdits = 0;
|
|
||||||
m_currentMessagePendingEdits = 0;
|
|
||||||
m_currentMessageRejectedEdits = 0;
|
|
||||||
emit currentMessageEditsStatsChanged();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
|
|
||||||
int total = edits.size();
|
|
||||||
int applied = 0;
|
|
||||||
int pending = 0;
|
|
||||||
int rejected = 0;
|
|
||||||
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
switch (edit.status) {
|
|
||||||
case Context::ChangesManager::Applied:
|
|
||||||
applied++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Pending:
|
|
||||||
pending++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Rejected:
|
|
||||||
rejected++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Archived:
|
|
||||||
total--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (m_currentMessageTotalEdits != total) {
|
|
||||||
m_currentMessageTotalEdits = total;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessageAppliedEdits != applied) {
|
|
||||||
m_currentMessageAppliedEdits = applied;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessagePendingEdits != pending) {
|
|
||||||
m_currentMessagePendingEdits = pending;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessageRejectedEdits != rejected) {
|
|
||||||
m_currentMessageRejectedEdits = rejected;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
|
||||||
.arg(total).arg(applied).arg(pending).arg(rejected));
|
|
||||||
emit currentMessageEditsStatsChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::currentMessageTotalEdits() const
|
|
||||||
{
|
|
||||||
return m_currentMessageTotalEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::currentMessageAppliedEdits() const
|
|
||||||
{
|
|
||||||
return m_currentMessageAppliedEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::currentMessagePendingEdits() const
|
|
||||||
{
|
|
||||||
return m_currentMessagePendingEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::currentMessageRejectedEdits() const
|
|
||||||
{
|
|
||||||
return m_currentMessageRejectedEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::lastInfoMessage() const
|
|
||||||
{
|
|
||||||
return m_lastInfoMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::isThinkingSupport() const
|
|
||||||
{
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
|
||||||
return provider && provider->supportThinking();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "llmcore/PromptProviderChat.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -38,26 +37,6 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||||
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged 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)
|
|
||||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
|
||||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
|
||||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
|
|
||||||
Q_PROPERTY(
|
|
||||||
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
|
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
|
||||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
|
||||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
|
||||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@@ -86,7 +65,8 @@ public:
|
|||||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
Q_INVOKABLE void testRAG(const QString &message);
|
||||||
|
Q_INVOKABLE void testChunking();
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
@@ -99,48 +79,6 @@ public:
|
|||||||
|
|
||||||
QString chatFileName() const;
|
QString chatFileName() const;
|
||||||
void setRecentFilePath(const QString &filePath);
|
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);
|
|
||||||
|
|
||||||
QString lastErrorMessage() const;
|
|
||||||
|
|
||||||
QVariantList activeRules() const;
|
|
||||||
int activeRulesCount() const;
|
|
||||||
Q_INVOKABLE QString getRuleContent(int index);
|
|
||||||
Q_INVOKABLE void refreshRules();
|
|
||||||
|
|
||||||
bool isAgentMode() const;
|
|
||||||
void setIsAgentMode(bool newIsAgentMode);
|
|
||||||
bool isThinkingMode() const;
|
|
||||||
void setIsThinkingMode(bool newIsThinkingMode);
|
|
||||||
bool toolsSupportEnabled() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
|
||||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
|
||||||
Q_INVOKABLE void undoFileEdit(const QString &editId);
|
|
||||||
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
|
|
||||||
|
|
||||||
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
|
||||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
|
||||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
|
||||||
|
|
||||||
int currentMessageTotalEdits() const;
|
|
||||||
int currentMessageAppliedEdits() const;
|
|
||||||
int currentMessagePendingEdits() const;
|
|
||||||
int currentMessageRejectedEdits() const;
|
|
||||||
|
|
||||||
QString lastInfoMessage() const;
|
|
||||||
|
|
||||||
bool isThinkingSupport() const;
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
@@ -157,33 +95,12 @@ signals:
|
|||||||
void inputTokensCountChanged();
|
void inputTokensCountChanged();
|
||||||
void isSyncOpenFilesChanged();
|
void isSyncOpenFilesChanged();
|
||||||
void chatFileNameChanged();
|
void chatFileNameChanged();
|
||||||
void textFamilyChanged();
|
|
||||||
void codeFamilyChanged();
|
|
||||||
void codeFontSizeChanged();
|
|
||||||
void textFontSizeChanged();
|
|
||||||
void textFormatChanged();
|
|
||||||
void chatRequestStarted();
|
|
||||||
void isRequestInProgressChanged();
|
|
||||||
|
|
||||||
void lastErrorMessageChanged();
|
|
||||||
void lastInfoMessageChanged();
|
|
||||||
void activeRulesChanged();
|
|
||||||
void activeRulesCountChanged();
|
|
||||||
|
|
||||||
void isAgentModeChanged();
|
|
||||||
void isThinkingModeChanged();
|
|
||||||
void toolsSupportEnabledChanged();
|
|
||||||
void currentMessageEditsStatsChanged();
|
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
QString getSuggestedFileName() const;
|
QString getSuggestedFileName() const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
LLMCore::PromptProviderChat m_promptProvider;
|
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
@@ -193,18 +110,6 @@ private:
|
|||||||
int m_inputTokensCount{0};
|
int m_inputTokensCount{0};
|
||||||
bool m_isSyncOpenFiles;
|
bool m_isSyncOpenFiles;
|
||||||
QList<Core::IEditor *> m_currentEditors;
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
bool m_isRequestInProgress;
|
|
||||||
QString m_lastErrorMessage;
|
|
||||||
QVariantList m_activeRules;
|
|
||||||
bool m_isAgentMode;
|
|
||||||
bool m_isThinkingMode;
|
|
||||||
|
|
||||||
QString m_currentMessageRequestId;
|
|
||||||
int m_currentMessageTotalEdits{0};
|
|
||||||
int m_currentMessageAppliedEdits{0};
|
|
||||||
int m_currentMessagePendingEdits{0};
|
|
||||||
int m_currentMessageRejectedEdits{0};
|
|
||||||
QString m_lastInfoMessage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -83,10 +83,6 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
|
|||||||
messageObj["role"] = static_cast<int>(message.role);
|
messageObj["role"] = static_cast<int>(message.role);
|
||||||
messageObj["content"] = message.content;
|
messageObj["content"] = message.content;
|
||||||
messageObj["id"] = message.id;
|
messageObj["id"] = message.id;
|
||||||
messageObj["isRedacted"] = message.isRedacted;
|
|
||||||
if (!message.signature.isEmpty()) {
|
|
||||||
messageObj["signature"] = message.signature;
|
|
||||||
}
|
|
||||||
return messageObj;
|
return messageObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +92,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
|
|||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||||
message.content = json["content"].toString();
|
message.content = json["content"].toString();
|
||||||
message.id = json["id"].toString();
|
message.id = json["id"].toString();
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
|
||||||
message.signature = json["signature"].toString();
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,14 +120,9 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
|||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
model->clear();
|
||||||
|
|
||||||
model->setLoadingFromHistory(true);
|
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
for (const auto &message : messages) {
|
||||||
model->addMessage(message.content, message.role, message.id);
|
model->addMessage(message.content, message.role, message.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -29,40 +29,4 @@ void ChatUtils::copyToClipboard(const QString &text)
|
|||||||
QGuiApplication::clipboard()->setText(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
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -34,7 +34,6 @@ public:
|
|||||||
: QObject(parent) {};
|
: QObject(parent) {};
|
||||||
|
|
||||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||||
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
|
|||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||||
}
|
}
|
||||||
} // namespace QodeAssist::Chat
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -38,4 +38,4 @@ signals:
|
|||||||
void clearPressed();
|
void clearPressed();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,58 +19,58 @@
|
|||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
#include <texteditor/textdocument.h>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <coreplugin/editormanager/ieditor.h>
|
#include <coreplugin/editormanager/ieditor.h>
|
||||||
#include <coreplugin/idocument.h>
|
#include <coreplugin/idocument.h>
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectexplorer.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
|
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ContextManager.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
#include "PromptTemplateManager.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "RequestConfig.hpp"
|
|
||||||
#include <context/ChangesManager.h>
|
|
||||||
#include <RulesLoader.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(
|
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
, m_requestHandler(new LLMCore::RequestHandler(this))
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
, m_promptProvider(promptProvider)
|
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
|
||||||
{}
|
|
||||||
|
|
||||||
ClientInterface::~ClientInterface()
|
|
||||||
{
|
{
|
||||||
cancelRequest();
|
connect(m_requestHandler,
|
||||||
|
&LLMCore::RequestHandler::completionReceived,
|
||||||
|
this,
|
||||||
|
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
||||||
|
handleLLMResponse(completion, request, isComplete);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_requestHandler,
|
||||||
|
&LLMCore::RequestHandler::requestFinished,
|
||||||
|
this,
|
||||||
|
[this](const QString &, bool success, const QString &errorString) {
|
||||||
|
if (!success) {
|
||||||
|
emit errorOccurred(errorString);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientInterface::~ClientInterface() = default;
|
||||||
|
|
||||||
void ClientInterface::sendMessage(
|
void ClientInterface::sendMessage(
|
||||||
const QString &message,
|
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
||||||
const QList<QString> &attachments,
|
|
||||||
const QList<QString> &linkedFiles,
|
|
||||||
bool useAgentMode)
|
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
|
|
||||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
|
||||||
|
|
||||||
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
@@ -84,7 +84,8 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
if (!promptTemplate) {
|
if (!promptTemplate) {
|
||||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||||
@@ -92,136 +93,52 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData context;
|
LLMCore::ContextData context;
|
||||||
|
context.prefix = message;
|
||||||
|
context.suffix = "";
|
||||||
|
|
||||||
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
QString systemPrompt;
|
||||||
|
if (chatAssistantSettings.useSystemPrompt())
|
||||||
|
systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
|
||||||
|
|
||||||
if (project) {
|
|
||||||
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
|
||||||
systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString());
|
|
||||||
|
|
||||||
QString projectRules
|
|
||||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemPrompt += QString("\n# No active project in IDE");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
|
||||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
|
||||||
}
|
|
||||||
context.systemPrompt = systemPrompt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::Message> messages;
|
QJsonObject providerRequest;
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
providerRequest["model"] = Settings::generalSettings().caModel();
|
||||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
providerRequest["stream"] = chatAssistantSettings.stream();
|
||||||
continue;
|
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
|
||||||
}
|
|
||||||
|
if (promptTemplate)
|
||||||
LLMCore::Message apiMessage;
|
promptTemplate->prepareRequest(providerRequest, context);
|
||||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
else
|
||||||
apiMessage.content = msg.content;
|
qWarning("No prompt template found");
|
||||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
|
||||||
apiMessage.isRedacted = msg.isRedacted;
|
if (provider)
|
||||||
apiMessage.signature = msg.signature;
|
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
|
||||||
|
else
|
||||||
messages.append(apiMessage);
|
qWarning("No provider found");
|
||||||
}
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
LLMCore::LLMConfig config;
|
LLMCore::LLMConfig config;
|
||||||
config.requestType = LLMCore::RequestType::Chat;
|
config.requestType = LLMCore::RequestType::Chat;
|
||||||
config.provider = provider;
|
config.provider = provider;
|
||||||
config.promptTemplate = promptTemplate;
|
config.promptTemplate = promptTemplate;
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
config.providerRequest = providerRequest;
|
||||||
config.url = QUrl(QString("%1/models/%2:%3")
|
config.multiLineCompletion = false;
|
||||||
.arg(
|
|
||||||
Settings::generalSettings().caUrl(),
|
|
||||||
Settings::generalSettings().caModel(),
|
|
||||||
stream));
|
|
||||||
} else {
|
|
||||||
config.url
|
|
||||||
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
|
||||||
config.providerRequest
|
|
||||||
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
|
||||||
}
|
|
||||||
|
|
||||||
config.apiKey = provider->apiKey();
|
config.apiKey = provider->apiKey();
|
||||||
|
|
||||||
config.provider->prepareRequest(
|
QJsonObject request;
|
||||||
config.providerRequest,
|
request["id"] = QUuid::createUuid().toString();
|
||||||
promptTemplate,
|
|
||||||
context,
|
|
||||||
LLMCore::RequestType::Chat,
|
|
||||||
isToolsEnabled,
|
|
||||||
Settings::chatAssistantSettings().enableThinkingMode());
|
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||||
QJsonObject request{{"id", requestId}};
|
if (!errors.isEmpty()) {
|
||||||
|
LOG_MESSAGE("Validate errors for chat request:");
|
||||||
|
LOG_MESSAGES(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
m_requestHandler->sendLLMRequest(config, request);
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::partialResponseReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handlePartialResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::requestFailed,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::toolExecutionStarted,
|
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::addToolExecutionStatus,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::toolExecutionCompleted,
|
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::updateToolResult,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::continuationStarted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleCleanAccumulatedData,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::thinkingBlockReceived,
|
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::addThinkingBlock,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::addRedactedThinkingBlock,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
@@ -232,37 +149,25 @@ void ClientInterface::clearMessages()
|
|||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
QSet<LLMCore::Provider *> providers;
|
auto id = m_chatModel->lastMessageId();
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
m_requestHandler->cancelRequest(id);
|
||||||
if (it.value().provider) {
|
|
||||||
providers.insert(it.value().provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeRequests.clear();
|
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
void ClientInterface::handleLLMResponse(const QString &response,
|
||||||
|
const QJsonObject &request,
|
||||||
|
bool isComplete)
|
||||||
{
|
{
|
||||||
const auto message = response.trimmed();
|
const auto message = response.trimmed();
|
||||||
|
|
||||||
if (!message.isEmpty()) {
|
if (!message.isEmpty()) {
|
||||||
QString messageId = request["id"].toString();
|
QString messageId = request["id"].toString();
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
||||||
|
|
||||||
|
if (isComplete) {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
"Message completed. Final response for message " + messageId + ": " + response);
|
||||||
|
emit messageReceivedCompletely();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,102 +186,30 @@ QString ClientInterface::getCurrentFileContext() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
|
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
|
||||||
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
|
.arg(textDocument->mimeType(), textDocument->filePath().toString());
|
||||||
|
|
||||||
QString content = textDocument->document()->toPlainText();
|
QString content = textDocument->document()->toPlainText();
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
|
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
|
||||||
|
|
||||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
|
||||||
{
|
{
|
||||||
QString updatedPrompt = basePrompt;
|
QString updatedPrompt = basePrompt;
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||||
|
|
||||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
|
||||||
for (const auto &file : contentFiles) {
|
for (const auto &file : contentFiles) {
|
||||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
|
||||||
|
.arg(file.filename, file.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedPrompt;
|
return updatedPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Context::ContextManager *ClientInterface::contextManager() const
|
|
||||||
{
|
|
||||||
return m_contextManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_accumulatedResponses[requestId] += partialText;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
|
||||||
|
|
||||||
QString applyError;
|
|
||||||
bool applySuccess = Context::ChangesManager::instance()
|
|
||||||
.applyPendingEditsForRequest(requestId, &applyError);
|
|
||||||
|
|
||||||
if (!applySuccess) {
|
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
|
||||||
.arg(requestId, applyError));
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
|
|
||||||
+ ": " + finalText);
|
|
||||||
emit messageReceivedCompletely();
|
|
||||||
|
|
||||||
if (it != m_activeRequests.end()) {
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
}
|
|
||||||
if (m_accumulatedResponses.contains(requestId)) {
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
|
||||||
emit errorOccurred(error);
|
|
||||||
|
|
||||||
if (it != m_activeRequests.end()) {
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
}
|
|
||||||
if (m_accumulatedResponses.contains(requestId)) {
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
|
||||||
{
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -24,9 +24,7 @@
|
|||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include "RequestHandler.hpp"
|
||||||
#include "llmcore/IPromptProvider.hpp"
|
|
||||||
#include <context/ContextManager.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -35,49 +33,29 @@ class ClientInterface : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(
|
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void sendMessage(
|
void sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
const QList<QString> &linkedFiles = {},
|
const QList<QString> &linkedFiles = {});
|
||||||
bool useAgentMode = false);
|
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
Context::ContextManager *contextManager() const;
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
void messageReceivedCompletely();
|
void messageReceivedCompletely();
|
||||||
void requestStarted(const QString &requestId);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
void handleCleanAccumulatedData(const QString &requestId);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||||
QString getCurrentFileContext() const;
|
QString getCurrentFileContext() const;
|
||||||
QString getSystemPromptWithLinkedFiles(
|
QString getSystemPromptWithLinkedFiles(
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
const QString &basePrompt,
|
||||||
|
const QList<QString> &linkedFiles) const;
|
||||||
|
|
||||||
struct RequestContext
|
LLMCore::RequestHandler *m_requestHandler;
|
||||||
{
|
|
||||||
QJsonObject originalRequest;
|
|
||||||
LLMCore::Provider *provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
|
||||||
QHash<QString, QString> m_accumulatedResponses;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,24 +19,33 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <qobject.h>
|
||||||
#include <QtQmlIntegration>
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
#include "ChatData.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
Q_NAMESPACE
|
||||||
|
|
||||||
class MessagePart
|
class MessagePart
|
||||||
{
|
{
|
||||||
Q_GADGET
|
Q_GADGET
|
||||||
Q_PROPERTY(MessagePartType type MEMBER type CONSTANT FINAL)
|
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
|
||||||
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
||||||
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
||||||
QML_VALUE_TYPE(messagePart)
|
QML_VALUE_TYPE(messagePart)
|
||||||
public:
|
public:
|
||||||
MessagePartType type;
|
enum PartType { Code, Text };
|
||||||
|
Q_ENUM(PartType)
|
||||||
|
|
||||||
|
PartType type;
|
||||||
QString text;
|
QString text;
|
||||||
QString language;
|
QString language;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MessagePartType : public MessagePart
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
};
|
||||||
|
|
||||||
|
QML_NAMED_ELEMENT(MessagePart)
|
||||||
|
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_74_61)">
|
|
||||||
<mask id="mask0_74_61" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_61)">
|
|
||||||
<path d="M8 22L18 32L36 12" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_61">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 548 B |
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 523 B |
@@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 548 B |
@@ -1,8 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 822 B |
@@ -1,12 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 624 B |
@@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 370 B |
@@ -1,17 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_74_52)">
|
|
||||||
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_52)">
|
|
||||||
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
|
||||||
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_52">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 943 B |
@@ -1,16 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_74_76)">
|
|
||||||
<mask id="mask0_74_76" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_76)">
|
|
||||||
<path d="M12 12L32 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
|
||||||
<path d="M32 12L12 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_76">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 599 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M35.75 2.75H8.25C6.73122 2.75 5.5 3.98122 5.5 5.5V38.5C5.5 40.0188 6.73122 41.25 8.25 41.25H35.75C37.2688 41.25 38.5 40.0188 38.5 38.5V5.5C38.5 3.98122 37.2688 2.75 35.75 2.75Z" stroke="black" stroke-width="4"/>
|
|
||||||
<path d="M13.75 14.4375C14.8891 14.4375 15.8125 13.5141 15.8125 12.375C15.8125 11.2359 14.8891 10.3125 13.75 10.3125C12.6109 10.3125 11.6875 11.2359 11.6875 12.375C11.6875 13.5141 12.6109 14.4375 13.75 14.4375Z" fill="black"/>
|
|
||||||
<path d="M19.25 12.375H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
|
||||||
<path d="M13.75 24.0625C14.8891 24.0625 15.8125 23.1391 15.8125 22C15.8125 20.8609 14.8891 19.9375 13.75 19.9375C12.6109 19.9375 11.6875 20.8609 11.6875 22C11.6875 23.1391 12.6109 24.0625 13.75 24.0625Z" fill="black"/>
|
|
||||||
<path d="M19.25 22H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
|
||||||
<path d="M13.75 33.6875C14.8891 33.6875 15.8125 32.7641 15.8125 31.625C15.8125 30.4859 14.8891 29.5625 13.75 29.5625C12.6109 29.5625 11.6875 30.4859 11.6875 31.625C11.6875 32.7641 12.6109 33.6875 13.75 33.6875Z" fill="black"/>
|
|
||||||
<path d="M19.25 31.625H27.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 370 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
|
|
||||||
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,16 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_74_68)">
|
|
||||||
<mask id="mask0_74_68" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_68)">
|
|
||||||
<path d="M12 12L6 18L12 24" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M6 18H28C33 18 38 23 38 28C38 33 33 38 28 38H22" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_68">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 663 B |
@@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 552 B |
@@ -1,5 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 559 B |
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -23,7 +23,6 @@ Rectangle {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias text: badgeText.text
|
property alias text: badgeText.text
|
||||||
property alias hovered: mouse.hovered
|
|
||||||
|
|
||||||
implicitWidth: badgeText.implicitWidth + root.radius
|
implicitWidth: badgeText.implicitWidth + root.radius
|
||||||
implicitHeight: badgeText.implicitHeight + 6
|
implicitHeight: badgeText.implicitHeight + 6
|
||||||
@@ -38,10 +37,4 @@ Rectangle {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: palette.buttonText
|
color: palette.buttonText
|
||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: mouse
|
|
||||||
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,10 +19,7 @@
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ChatView
|
import ChatView
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import UIControls
|
|
||||||
|
|
||||||
import "./dialog"
|
import "./dialog"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -30,42 +27,14 @@ Rectangle {
|
|||||||
|
|
||||||
property alias msgModel: msgCreator.model
|
property alias msgModel: msgCreator.model
|
||||||
property alias messageAttachments: attachmentsModel.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
|
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
|
||||||
|
|
||||||
height: msgColumn.implicitHeight + 10
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
color: isUserMessage ? palette.alternateBase
|
|
||||||
: palette.base
|
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: mouse
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: msgColumn
|
id: msgColumn
|
||||||
|
|
||||||
x: 5
|
width: parent.width
|
||||||
width: parent.width - x
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
@@ -87,8 +56,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch(modelData.type) {
|
switch(modelData.type) {
|
||||||
case MessagePartType.Text: return textComponent;
|
case MessagePart.Text: return textComponent;
|
||||||
case MessagePartType.Code: return codeBlockComponent;
|
case MessagePart.Code: return codeBlockComponent;
|
||||||
default: return textComponent;
|
default: return textComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,66 +113,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: userMessageMarker
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 3
|
|
||||||
height: root.height - root.radius
|
|
||||||
color: "#92BD6C"
|
|
||||||
radius: root.radius
|
|
||||||
visible: root.isUserMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: stopButtonId
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
right: parent.right
|
|
||||||
top: parent.top
|
|
||||||
}
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
visible: root.isUserMessage && mouse.hovered
|
|
||||||
onClicked: function() {
|
|
||||||
root.resetChatToMessage(root.messageIndex)
|
|
||||||
}
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
|
||||||
ToolTip.delay: 500
|
|
||||||
}
|
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
component TextComponent : TextBlock {
|
||||||
required property var itemData
|
required property var itemData
|
||||||
height: implicitHeight + 10
|
height: implicitHeight + 10
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
leftPadding: 10
|
leftPadding: 10
|
||||||
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
text: 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
|
|
||||||
|
|
||||||
|
component CodeBlockComponent : CodeBlock {
|
||||||
required property var itemData
|
required property var itemData
|
||||||
anchors {
|
anchors {
|
||||||
left: parent.left
|
left: parent.left
|
||||||
@@ -214,7 +133,5 @@ Rectangle {
|
|||||||
|
|
||||||
code: itemData.text
|
code: itemData.text
|
||||||
language: itemData.language
|
language: itemData.language
|
||||||
codeFontFamily: root.codeFontFamily
|
|
||||||
codeFontSize: root.codeFontSize
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,469 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 QtQuick.Layouts
|
|
||||||
import UIControls
|
|
||||||
import ChatView
|
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string editContent: ""
|
|
||||||
|
|
||||||
readonly property var editData: parseEditData(editContent)
|
|
||||||
readonly property string filePath: editData.file || ""
|
|
||||||
readonly property string fileName: getFileName(filePath)
|
|
||||||
readonly property string editStatus: editData.status || "pending"
|
|
||||||
readonly property string statusMessage: editData.status_message || ""
|
|
||||||
readonly property string oldContent: editData.old_content || ""
|
|
||||||
readonly property string newContent: editData.new_content || ""
|
|
||||||
|
|
||||||
signal applyEdit(string editId)
|
|
||||||
signal rejectEdit(string editId)
|
|
||||||
signal undoEdit(string editId)
|
|
||||||
signal openInEditor(string editId)
|
|
||||||
|
|
||||||
readonly property int borderRadius: 4
|
|
||||||
readonly property int contentMargin: 10
|
|
||||||
readonly property int contentBottomPadding: 20
|
|
||||||
readonly property int headerPadding: 8
|
|
||||||
readonly property int statusIndicatorWidth: 4
|
|
||||||
|
|
||||||
readonly property bool isPending: editStatus === "pending"
|
|
||||||
readonly property bool isApplied: editStatus === "applied"
|
|
||||||
readonly property bool isRejected: editStatus === "rejected"
|
|
||||||
readonly property bool isArchived: editStatus === "archived"
|
|
||||||
|
|
||||||
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
|
||||||
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
|
||||||
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
|
||||||
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
|
||||||
readonly property color pendingColor: palette.highlight
|
|
||||||
|
|
||||||
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
|
||||||
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
|
|
||||||
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
|
|
||||||
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
|
|
||||||
|
|
||||||
readonly property string codeFontFamily: {
|
|
||||||
switch (Qt.platform.os) {
|
|
||||||
case "windows": return "Consolas"
|
|
||||||
case "osx": return "Menlo"
|
|
||||||
case "linux": return "DejaVu Sans Mono"
|
|
||||||
default: return "monospace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readonly property int codeFontSize: Qt.application.font.pointSize
|
|
||||||
|
|
||||||
readonly property color statusColor: {
|
|
||||||
if (isArchived) return archivedColor
|
|
||||||
if (isApplied) return appliedColor
|
|
||||||
if (isRejected) return rejectedColor
|
|
||||||
return pendingColor
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property color statusBgColor: {
|
|
||||||
if (isArchived) return archivedBgColor
|
|
||||||
if (isApplied) return appliedBgColor
|
|
||||||
if (isRejected) return rejectedBgColor
|
|
||||||
return palette.button
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property string statusText: {
|
|
||||||
if (isArchived) return qsTr("ARCHIVED")
|
|
||||||
if (isApplied) return qsTr("APPLIED")
|
|
||||||
if (isRejected) return qsTr("REJECTED")
|
|
||||||
return qsTr("PENDING")
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int addedLines: countLines(newContent)
|
|
||||||
readonly property int removedLines: countLines(oldContent)
|
|
||||||
|
|
||||||
function parseEditData(content) {
|
|
||||||
try {
|
|
||||||
const marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
let jsonStr = content;
|
|
||||||
if (content.indexOf(marker) >= 0) {
|
|
||||||
jsonStr = content.substring(content.indexOf(marker) + marker.length);
|
|
||||||
}
|
|
||||||
return JSON.parse(jsonStr);
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
edit_id: "",
|
|
||||||
file: "",
|
|
||||||
old_content: "",
|
|
||||||
new_content: "",
|
|
||||||
status: "error",
|
|
||||||
status_message: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileName(path) {
|
|
||||||
if (!path) return "";
|
|
||||||
const parts = path.split('/');
|
|
||||||
return parts[parts.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function countLines(text) {
|
|
||||||
if (!text) return 0;
|
|
||||||
return text.split('\n').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: fileEditView.implicitHeight
|
|
||||||
|
|
||||||
ChatUtils {
|
|
||||||
id: utils
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: fileEditView
|
|
||||||
|
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
|
|
||||||
: headerArea.height
|
|
||||||
radius: root.borderRadius
|
|
||||||
|
|
||||||
color: palette.base
|
|
||||||
|
|
||||||
border.width: 1
|
|
||||||
border.color: root.isPending
|
|
||||||
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
|
|
||||||
: Qt.alpha(root.statusColor, 0.6)
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "expanded"
|
|
||||||
when: fileEditView.expanded
|
|
||||||
PropertyChanges { target: contentColumn; opacity: 1 }
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "collapsed"
|
|
||||||
when: !fileEditView.expanded
|
|
||||||
PropertyChanges { target: contentColumn; opacity: 0 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
transitions: Transition {
|
|
||||||
NumberAnimation {
|
|
||||||
properties: "opacity"
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: headerArea
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: headerRow.height + 16
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: fileEditView.expanded = !fileEditView.expanded
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
left: parent.left
|
|
||||||
right: actionButtons.left
|
|
||||||
leftMargin: root.contentMargin
|
|
||||||
rightMargin: root.contentMargin
|
|
||||||
}
|
|
||||||
spacing: root.headerPadding
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: root.statusIndicatorWidth
|
|
||||||
height: headerText.height
|
|
||||||
radius: 2
|
|
||||||
color: root.statusColor
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: headerText
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: {
|
|
||||||
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
|
||||||
if (root.oldContent.length > 0) {
|
|
||||||
return qsTr("%1: %2 (+%3 -%4)")
|
|
||||||
.arg(modeText)
|
|
||||||
.arg(root.fileName)
|
|
||||||
.arg(root.addedLines)
|
|
||||||
.arg(root.removedLines)
|
|
||||||
} else {
|
|
||||||
return qsTr("%1: %2 (+%3)")
|
|
||||||
.arg(modeText)
|
|
||||||
.arg(root.fileName)
|
|
||||||
.arg(root.addedLines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: fileEditView.expanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: !root.isPending
|
|
||||||
Layout.preferredWidth: badgeText.width + 12
|
|
||||||
Layout.preferredHeight: badgeText.height + 4
|
|
||||||
color: root.statusBgColor
|
|
||||||
radius: 3
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: badgeText
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.statusText
|
|
||||||
font.pixelSize: 9
|
|
||||||
font.bold: true
|
|
||||||
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: actionButtons
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 5
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: root.openInEditor(editData.edit_id)
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
|
||||||
ToolTip.delay: 500
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
} enabled: (root.isPending || root.isRejected) && !root.isArchived
|
|
||||||
visible: !root.isApplied && !root.isArchived
|
|
||||||
onClicked: root.applyEdit(editData.edit_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
enabled: root.isApplied && !root.isArchived
|
|
||||||
visible: root.isApplied && !root.isArchived
|
|
||||||
onClicked: root.undoEdit(editData.edit_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
enabled: root.isPending && !root.isArchived
|
|
||||||
visible: root.isPending && !root.isArchived
|
|
||||||
onClicked: root.rejectEdit(editData.edit_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
top: headerArea.bottom
|
|
||||||
margins: root.contentMargin
|
|
||||||
}
|
|
||||||
spacing: 8
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: root.filePath
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
|
|
||||||
color: Qt.rgba(1, 0.2, 0.2, 0.1)
|
|
||||||
radius: 4
|
|
||||||
border.width: 1
|
|
||||||
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
|
|
||||||
visible: root.oldContent.length > 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: oldContentColumn
|
|
||||||
width: parent.width
|
|
||||||
x: 6
|
|
||||||
y: 6
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: oldContentText
|
|
||||||
|
|
||||||
width: parent.width - 12
|
|
||||||
height: contentHeight
|
|
||||||
text: root.oldContent
|
|
||||||
font.family: root.codeFontFamily
|
|
||||||
font.pixelSize: root.codeFontSize
|
|
||||||
color: palette.text
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
|
||||||
textFormat: TextEdit.PlainText
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: oldConentContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: oldConentContextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
onTriggered: {
|
|
||||||
const textToCopy = oldContentText.selectedText || root.oldContent
|
|
||||||
utils.copyToClipboard(textToCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
|
|
||||||
onTriggered: fileEditView.expanded = !fileEditView.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: newContentColumn.implicitHeight + 12
|
|
||||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
|
|
||||||
radius: 4
|
|
||||||
border.width: 1
|
|
||||||
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: newContentColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
x: 6
|
|
||||||
y: 6
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: newContentText
|
|
||||||
|
|
||||||
width: parent.width - 12
|
|
||||||
height: contentHeight
|
|
||||||
text: root.newContent
|
|
||||||
font.family: root.codeFontFamily
|
|
||||||
font.pixelSize: root.codeFontSize
|
|
||||||
color: palette.text
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
|
||||||
textFormat: TextEdit.PlainText
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: newContentContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: newContentContextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
onTriggered: {
|
|
||||||
const textToCopy = newContentText.selectedText || root.newContent
|
|
||||||
utils.copyToClipboard(textToCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
|
|
||||||
onTriggered: fileEditView.expanded = !fileEditView.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: root.statusMessage.length > 0
|
|
||||||
text: root.statusMessage
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.italic: true
|
|
||||||
color: root.isApplied
|
|
||||||
? Qt.rgba(0.2, 0.6, 0.2, 1)
|
|
||||||
: Qt.rgba(0.8, 0.2, 0.2, 1)
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -22,8 +22,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Controls.Basic as QQC
|
import QtQuick.Controls.Basic as QQC
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
import "./controls"
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
import "./parts"
|
import "./parts"
|
||||||
|
|
||||||
ChatRootView {
|
ChatRootView {
|
||||||
@@ -65,80 +64,40 @@ ChatRootView {
|
|||||||
id: topBar
|
id: topBar
|
||||||
|
|
||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: childrenRect.height + 10
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
saveButton.onClicked: root.showSaveDialog()
|
saveButton.onClicked: root.showSaveDialog()
|
||||||
loadButton.onClicked: root.showLoadDialog()
|
loadButton.onClicked: root.showLoadDialog()
|
||||||
clearButton.onClicked: root.clearChat()
|
clearButton.onClicked: root.clearChat()
|
||||||
tokensBadge {
|
tokensBadge {
|
||||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||||
}
|
}
|
||||||
recentPath {
|
recentPath {
|
||||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
}
|
}
|
||||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||||
rulesButton.onClicked: rulesViewer.open()
|
|
||||||
activeRulesCount: root.activeRulesCount
|
|
||||||
pinButton {
|
|
||||||
visible: typeof _chatview !== 'undefined'
|
|
||||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
|
||||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
|
||||||
}
|
|
||||||
agentModeSwitch {
|
|
||||||
checked: root.isAgentMode
|
|
||||||
enabled: root.toolsSupportEnabled
|
|
||||||
onToggled: {
|
|
||||||
root.isAgentMode = agentModeSwitch.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thinkingMode {
|
|
||||||
checked: root.isThinkingMode
|
|
||||||
enabled: root.isThinkingSupport
|
|
||||||
onCheckedChanged: {
|
|
||||||
root.isThinkingMode = thinkingMode.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
signal hideServiceComponents(int itemIndex)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
leftMargin: 5
|
leftMargin: 5
|
||||||
model: root.chatModel
|
model: root.chatModel
|
||||||
clip: true
|
clip: true
|
||||||
spacing: 0
|
spacing: 10
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
delegate: Loader {
|
delegate: ChatItem {
|
||||||
id: componentLoader
|
|
||||||
|
|
||||||
required property var model
|
required property var model
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: ListView.view.width - scroll.width
|
width: ListView.view.width - scroll.width
|
||||||
|
msgModel: root.chatModel.processMessageContent(model.content)
|
||||||
sourceComponent: {
|
messageAttachments: model.attachments
|
||||||
if (model.roleType === ChatModel.Tool) {
|
color: model.roleType === ChatModel.User ? palette.alternateBase
|
||||||
return toolMessageComponent
|
: palette.base
|
||||||
} else if (model.roleType === ChatModel.FileEdit) {
|
|
||||||
return fileEditMessageComponent
|
|
||||||
} else if (model.roleType === ChatModel.Thinking) {
|
|
||||||
return thinkingMessageComponent
|
|
||||||
} else {
|
|
||||||
return chatItemComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
|
||||||
chatListView.hideServiceComponents(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -159,104 +118,6 @@ ChatRootView {
|
|||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
|
||||||
id: chatItemComponent
|
|
||||||
|
|
||||||
ChatItem {
|
|
||||||
id: chatItemInstance
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
msgModel: root.chatModel.processMessageContent(model.content)
|
|
||||||
messageAttachments: model.attachments
|
|
||||||
isUserMessage: model.roleType === ChatModel.User
|
|
||||||
messageIndex: index
|
|
||||||
textFontFamily: root.textFontFamily
|
|
||||||
codeFontFamily: root.codeFontFamily
|
|
||||||
codeFontSize: root.codeFontSize
|
|
||||||
textFontSize: root.textFontSize
|
|
||||||
textFormat: root.textFormat
|
|
||||||
|
|
||||||
onResetChatToMessage: function(idx) {
|
|
||||||
messageInput.text = model.content
|
|
||||||
messageInput.cursorPosition = model.content.length
|
|
||||||
root.chatModel.resetModelTo(idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: toolMessageComponent
|
|
||||||
|
|
||||||
ToolStatusItem {
|
|
||||||
id: toolsItem
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
toolContent: model.content
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
toolsItem.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: fileEditMessageComponent
|
|
||||||
|
|
||||||
FileEditItem {
|
|
||||||
width: parent.width
|
|
||||||
editContent: model.content
|
|
||||||
|
|
||||||
onApplyEdit: function(editId) {
|
|
||||||
root.applyFileEdit(editId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onRejectEdit: function(editId) {
|
|
||||||
root.rejectFileEdit(editId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onUndoEdit: function(editId) {
|
|
||||||
root.undoFileEdit(editId)
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenInEditor: function(editId) {
|
|
||||||
root.openFileEditInEditor(editId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: thinkingMessageComponent
|
|
||||||
|
|
||||||
ThinkingStatusItem {
|
|
||||||
id: thinking
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
thinkingContent: {
|
|
||||||
let content = model.content
|
|
||||||
let signatureStart = content.indexOf("\n[Signature:")
|
|
||||||
if (signatureStart >= 0) {
|
|
||||||
return content.substring(0, signatureStart)
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
thinking.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -269,9 +130,7 @@ ChatRootView {
|
|||||||
QQC.TextArea {
|
QQC.TextArea {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: Qt.platform.os === "osx"
|
placeholderText: qsTr("Type your message here...")
|
||||||
? qsTr("Type your message here... (⌘+↩ to send)")
|
|
||||||
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
|
||||||
placeholderTextColor: palette.mid
|
placeholderTextColor: palette.mid
|
||||||
color: palette.text
|
color: palette.text
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
@@ -294,53 +153,15 @@ ChatRootView {
|
|||||||
|
|
||||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
||||||
|
|
||||||
MouseArea {
|
Keys.onPressed: function(event) {
|
||||||
anchors.fill: parent
|
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
||||||
acceptedButtons: Qt.RightButton
|
root.sendChatMessage()
|
||||||
onClicked: messageContextMenu.open()
|
event.accepted = true;
|
||||||
propagateComposedEvents: true
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: messageContextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Cut")
|
|
||||||
enabled: messageInput.selectedText.length > 0
|
|
||||||
onTriggered: messageInput.cut()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: messageInput.selectedText.length > 0
|
|
||||||
onTriggered: messageInput.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Paste")
|
|
||||||
enabled: messageInput.canPaste
|
|
||||||
onTriggered: messageInput.paste()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Select All")
|
|
||||||
enabled: messageInput.text.length > 0
|
|
||||||
onTriggered: messageInput.selectAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Clear")
|
|
||||||
enabled: messageInput.text.length > 0
|
|
||||||
onTriggered: messageInput.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AttachedFilesPlace {
|
AttachedFilesPlace {
|
||||||
id: attachedFilesPlace
|
id: attachedFilesPlace
|
||||||
|
|
||||||
@@ -363,49 +184,22 @@ ChatRootView {
|
|||||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
FileEditsActionBar {
|
|
||||||
id: fileEditsActionBar
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
totalEdits: root.currentMessageTotalEdits
|
|
||||||
appliedEdits: root.currentMessageAppliedEdits
|
|
||||||
pendingEdits: root.currentMessagePendingEdits
|
|
||||||
rejectedEdits: root.currentMessageRejectedEdits
|
|
||||||
|
|
||||||
onApplyAllClicked: root.applyAllFileEditsForCurrentMessage()
|
|
||||||
onUndoAllClicked: root.undoAllFileEditsForCurrentMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomBar {
|
BottomBar {
|
||||||
id: bottomBar
|
id: bottomBar
|
||||||
|
|
||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
sendButton.onClicked: root.sendChatMessage()
|
||||||
: root.cancelRequest()
|
stopButton.onClicked: root.cancelRequest()
|
||||||
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
|
||||||
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
|
||||||
: qsTr("Stop")
|
|
||||||
syncOpenFiles {
|
syncOpenFiles {
|
||||||
checked: root.isSyncOpenFiles
|
checked: root.isSyncOpenFiles
|
||||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||||
}
|
}
|
||||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||||
}
|
testRag.onClicked: root.testRAG(messageInput.text)
|
||||||
}
|
testChunks.onClicked: root.testChunking()
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
id: sendMessageShortcut
|
|
||||||
|
|
||||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
|
||||||
context: Qt.WindowShortcut
|
|
||||||
onActivated: {
|
|
||||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
|
||||||
root.sendChatMessage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,55 +218,4 @@ ChatRootView {
|
|||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast {
|
|
||||||
id: errorToast
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
|
|
||||||
border.color: Qt.darker(infoToast.color, 1.3)
|
|
||||||
toastTextColor: "#FFFFFF"
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast {
|
|
||||||
id: infoToast
|
|
||||||
z: 1000
|
|
||||||
|
|
||||||
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
|
|
||||||
border.color: Qt.darker(infoToast.color, 1.3)
|
|
||||||
toastTextColor: "#FFFFFF"
|
|
||||||
}
|
|
||||||
|
|
||||||
RulesViewer {
|
|
||||||
id: rulesViewer
|
|
||||||
|
|
||||||
width: parent.width * 0.8
|
|
||||||
height: parent.height * 0.8
|
|
||||||
x: (parent.width - width) / 2
|
|
||||||
y: (parent.height - height) / 2
|
|
||||||
|
|
||||||
activeRules: root.activeRules
|
|
||||||
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
|
|
||||||
|
|
||||||
onRefreshRules: root.refreshRules()
|
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onLastErrorMessageChanged() {
|
|
||||||
if (root.lastErrorMessage.length > 0) {
|
|
||||||
errorToast.show(root.lastErrorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onLastInfoMessageChanged() {
|
|
||||||
if (root.lastInfoMessage.length > 0) {
|
|
||||||
infoToast.show(root.lastInfoMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
messageInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string thinkingContent: ""
|
|
||||||
// property string signature: ""
|
|
||||||
property bool isRedacted: false
|
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
property alias headerOpacity: headerRow.opacity
|
|
||||||
|
|
||||||
radius: 6
|
|
||||||
color: palette.base
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: header
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: headerRow.height + 10
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.expanded = !root.expanded
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 10
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.isRedacted ? qsTr("Thinking (Redacted)")
|
|
||||||
: qsTr("Thinking")
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.expanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
top: header.bottom
|
|
||||||
margins: 10
|
|
||||||
}
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.isRedacted
|
|
||||||
width: parent.width
|
|
||||||
text: qsTr("Thinking content was redacted by safety systems")
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.italic: true
|
|
||||||
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: thinkingText
|
|
||||||
|
|
||||||
visible: !root.isRedacted
|
|
||||||
width: parent.width
|
|
||||||
text: root.thinkingContent
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
color: palette.text
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rectangle {
|
|
||||||
// visible: root.signature.length > 0 && root.expanded
|
|
||||||
// width: parent.width
|
|
||||||
// height: signatureText.height + 10
|
|
||||||
// color: palette.alternateBase
|
|
||||||
// radius: 4
|
|
||||||
|
|
||||||
// Text {
|
|
||||||
// id: signatureText
|
|
||||||
|
|
||||||
// anchors {
|
|
||||||
// left: parent.left
|
|
||||||
// right: parent.right
|
|
||||||
// verticalCenter: parent.verticalCenter
|
|
||||||
// margins: 5
|
|
||||||
// }
|
|
||||||
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
|
|
||||||
// font.pixelSize: 9
|
|
||||||
// font.family: "monospace"
|
|
||||||
// color: palette.mid
|
|
||||||
// elide: Text.ElideRight
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: contextMenu.open()
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
|
||||||
onTriggered: root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: thinkingMarker
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 3
|
|
||||||
height: root.height - root.radius
|
|
||||||
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
|
|
||||||
: (root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
|
|
||||||
: Qt.lighter(palette.alternateBase, 1.3))
|
|
||||||
radius: root.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
when: !root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: header.height
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
when: root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: header.height + contentColumn.height + 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string toolContent: ""
|
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
property alias headerOpacity: headerRow.opacity
|
|
||||||
|
|
||||||
readonly property int firstNewline: toolContent.indexOf('\n')
|
|
||||||
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
|
|
||||||
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
|
|
||||||
|
|
||||||
radius: 6
|
|
||||||
color: palette.base
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: header
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: headerRow.height + 10
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.expanded = !root.expanded
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 10
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Tool: %1").arg(root.toolName)
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.expanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
right: parent.right
|
|
||||||
top: header.bottom
|
|
||||||
margins: 10
|
|
||||||
}
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: resultText
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
text: root.toolResult
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
color: palette.text
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: contextMenu.open()
|
|
||||||
propagateComposedEvents: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: resultText.selectedText.length > 0
|
|
||||||
onTriggered: resultText.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Select All")
|
|
||||||
enabled: resultText.text.length > 0
|
|
||||||
onTriggered: resultText.selectAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
|
||||||
onTriggered: root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: messageMarker
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 3
|
|
||||||
height: root.height - root.radius
|
|
||||||
color: root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
|
|
||||||
: Qt.lighter(palette.alternateBase, 1.3)
|
|
||||||
radius: root.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
when: !root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: header.height
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
when: root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: header.height + contentColumn.height + 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -20,127 +20,71 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string code: ""
|
property string code: ""
|
||||||
property string language: ""
|
property string language: ""
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
property alias codeFontFamily: codeText.font.family
|
readonly property string monospaceFont: {
|
||||||
property alias codeFontSize: codeText.font.pointSize
|
switch (Qt.platform.os) {
|
||||||
readonly property real collapsedHeight: copyButton.height + 10
|
case "windows":
|
||||||
|
return "Consolas";
|
||||||
|
case "osx":
|
||||||
|
return "Menlo";
|
||||||
|
case "linux":
|
||||||
|
return "DejaVu Sans Mono";
|
||||||
|
default:
|
||||||
|
return "monospace";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
color: palette.alternateBase
|
color: palette.alternateBase
|
||||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||||
: Qt.lighter(root.color, 1.3)
|
: Qt.lighter(root.color, 1.3)
|
||||||
border.width: 2
|
border.width: 2
|
||||||
radius: 4
|
radius: 4
|
||||||
implicitWidth: parent.width
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
implicitWidth: parent.width
|
||||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
implicitHeight: codeText.implicitHeight + 20
|
||||||
}
|
|
||||||
|
|
||||||
ChatUtils {
|
ChatUtils {
|
||||||
id: utils
|
id: utils
|
||||||
}
|
}
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: hoverHandler
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: header
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: root.collapsedHeight
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.expanded = !root.expanded
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: headerRow
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 10
|
|
||||||
}
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.language ? qsTr("Code (%1)").arg(root.language) :
|
|
||||||
qsTr("Code")
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.expanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: codeText
|
id: codeText
|
||||||
|
|
||||||
anchors {
|
anchors.fill: parent
|
||||||
left: parent.left
|
anchors.margins: 10
|
||||||
right: parent.right
|
|
||||||
top: header.bottom
|
|
||||||
margins: 10
|
|
||||||
}
|
|
||||||
text: root.code
|
text: root.code
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
|
font.family: root.monospaceFont
|
||||||
|
font.pointSize: Qt.application.font.pointSize
|
||||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
selectionColor: palette.highlight
|
selectionColor: palette.highlight
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: contextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.Menu {
|
TextEdit {
|
||||||
id: contextMenu
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
Platform.MenuItem {
|
anchors.margins: 5
|
||||||
text: qsTr("Copy")
|
readOnly: true
|
||||||
onTriggered: {
|
selectByMouse: true
|
||||||
const textToCopy = codeText.selectedText || root.code
|
text: root.language
|
||||||
utils.copyToClipboard(textToCopy)
|
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
||||||
}
|
: Qt.lighter(root.color, 1.1)
|
||||||
}
|
font.pointSize: 8
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
|
||||||
onTriggered: root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: copyButton
|
anchors.top: parent.top
|
||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 5
|
anchors.margins: 5
|
||||||
|
text: "Copy"
|
||||||
y: 5
|
|
||||||
text: qsTr("Copy")
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
utils.copyToClipboard(root.code)
|
utils.copyToClipboard(root.code)
|
||||||
text = qsTr("Copied")
|
text = qsTr("Copied")
|
||||||
@@ -153,21 +97,4 @@ Rectangle {
|
|||||||
onTriggered: parent.text = qsTr("Copy")
|
onTriggered: parent.text = qsTr("Copy")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
when: !root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: root.collapsedHeight
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
when: root.expanded
|
|
||||||
PropertyChanges {
|
|
||||||
target: root
|
|
||||||
implicitHeight: header.height + codeText.implicitHeight + 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: root
|
id: root
|
||||||
@@ -26,31 +25,7 @@ TextEdit {
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
textFormat: Text.StyledText
|
||||||
selectionColor: palette.highlight
|
selectionColor: palette.highlight
|
||||||
color: palette.text
|
color: palette.text
|
||||||
|
|
||||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.RightButton
|
|
||||||
onClicked: contextMenu.open()
|
|
||||||
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.Menu {
|
|
||||||
id: contextMenu
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: root.selectedText.length > 0
|
|
||||||
onTriggered: root.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.MenuItem {
|
|
||||||
text: qsTr("Select All")
|
|
||||||
enabled: root.text.length > 0
|
|
||||||
onTriggered: root.selectAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -21,16 +21,17 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias sendButton: sendButtonId
|
property alias sendButton: sendButtonId
|
||||||
|
property alias stopButton: stopButtonId
|
||||||
property alias syncOpenFiles: syncOpenFilesId
|
property alias syncOpenFiles: syncOpenFilesId
|
||||||
property alias attachFiles: attachFilesId
|
property alias attachFiles: attachFilesId
|
||||||
property alias linkFiles: linkFilesId
|
property alias linkFiles: linkFilesId
|
||||||
|
property alias testRag: testRagId
|
||||||
|
property alias testChunks: testChunksId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -52,12 +53,13 @@ Rectangle {
|
|||||||
QoAButton {
|
QoAButton {
|
||||||
id: sendButtonId
|
id: sendButtonId
|
||||||
|
|
||||||
icon {
|
text: qsTr("Send")
|
||||||
height: 15
|
}
|
||||||
width: 15
|
|
||||||
}
|
QoAButton {
|
||||||
ToolTip.visible: hovered
|
id: stopButtonId
|
||||||
ToolTip.delay: 250
|
|
||||||
|
text: qsTr("Stop")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -68,9 +70,7 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
text: qsTr("Attach files")
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Attach file to message")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -81,9 +81,7 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
text: qsTr("Link files")
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Link file to context")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -95,6 +93,18 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: testRagId
|
||||||
|
|
||||||
|
text: qsTr("Test RAG")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: testChunksId
|
||||||
|
|
||||||
|
text: qsTr("Test Chunks")
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 QtQuick.Layouts
|
|
||||||
import UIControls
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property int totalEdits: 0
|
|
||||||
property int appliedEdits: 0
|
|
||||||
property int pendingEdits: 0
|
|
||||||
property int rejectedEdits: 0
|
|
||||||
property bool hasAppliedEdits: appliedEdits > 0
|
|
||||||
property bool hasRejectedEdits: rejectedEdits > 0
|
|
||||||
property bool hasPendingEdits: pendingEdits > 0
|
|
||||||
|
|
||||||
signal applyAllClicked()
|
|
||||||
signal undoAllClicked()
|
|
||||||
|
|
||||||
visible: totalEdits > 0
|
|
||||||
implicitHeight: visible ? 40 : 0
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
|
||||||
Qt.darker(palette.window, 1.05) :
|
|
||||||
Qt.lighter(palette.window, 1.05)
|
|
||||||
|
|
||||||
border.width: 1
|
|
||||||
border.color: palette.mid
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 10
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 10
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.preferredWidth: 24
|
|
||||||
Layout.preferredHeight: 24
|
|
||||||
radius: 12
|
|
||||||
color: {
|
|
||||||
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
|
|
||||||
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
|
|
||||||
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
|
|
||||||
}
|
|
||||||
border.width: 2
|
|
||||||
border.color: {
|
|
||||||
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
|
|
||||||
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
|
||||||
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.totalEdits
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status text
|
|
||||||
ColumnLayout {
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.totalEdits === 1
|
|
||||||
? qsTr("File Edit in Current Message")
|
|
||||||
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.totalEdits > 0
|
|
||||||
text: {
|
|
||||||
let parts = [];
|
|
||||||
if (root.appliedEdits > 0) {
|
|
||||||
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
|
|
||||||
}
|
|
||||||
if (root.pendingEdits > 0) {
|
|
||||||
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
|
|
||||||
}
|
|
||||||
if (root.rejectedEdits > 0) {
|
|
||||||
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
|
|
||||||
}
|
|
||||||
return parts.join(", ");
|
|
||||||
}
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: applyAllButton
|
|
||||||
|
|
||||||
visible: root.hasPendingEdits || root.hasRejectedEdits
|
|
||||||
enabled: root.hasPendingEdits || root.hasRejectedEdits
|
|
||||||
text: root.hasPendingEdits
|
|
||||||
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
|
||||||
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: root.hasPendingEdits
|
|
||||||
? qsTr("Apply all pending and rejected edits in this message")
|
|
||||||
: qsTr("Reapply all rejected edits in this message")
|
|
||||||
|
|
||||||
onClicked: root.applyAllClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: undoAllButton
|
|
||||||
|
|
||||||
visible: root.hasAppliedEdits
|
|
||||||
enabled: root.hasAppliedEdits
|
|
||||||
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Undo all applied edits in this message")
|
|
||||||
|
|
||||||
onClicked: root.undoAllClicked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 QtQuick.Layouts
|
|
||||||
import QtQuick.Controls.Basic as QQC
|
|
||||||
|
|
||||||
import UIControls
|
|
||||||
import ChatView
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var activeRules
|
|
||||||
|
|
||||||
property alias rulesCurrentIndex: rulesList.currentIndex
|
|
||||||
property alias ruleContentAreaText: ruleContentArea.text
|
|
||||||
|
|
||||||
signal refreshRules()
|
|
||||||
signal openRulesFolder()
|
|
||||||
|
|
||||||
modal: true
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: palette.window
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatUtils {
|
|
||||||
id: utils
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Active Project Rules")
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Open Folder")
|
|
||||||
onClicked: root.openRulesFolder()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Refresh")
|
|
||||||
onClicked: root.refreshRules()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Close")
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
SplitView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
orientation: Qt.Horizontal
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.minimumWidth: 200
|
|
||||||
SplitView.preferredWidth: parent.width * 0.3
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Rules Files (%1)").arg(rulesList.count)
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: rulesList
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
model: root.activeRules
|
|
||||||
currentIndex: 0
|
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
highlighted: ListView.isCurrentItem
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: {
|
|
||||||
if (parent.highlighted) {
|
|
||||||
return palette.highlight
|
|
||||||
} else if (parent.hovered) {
|
|
||||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
|
||||||
}
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
radius: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: modelData.fileName
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Category: %1").arg(modelData.category)
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
rulesList.currentIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
id: scroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: rulesList.count === 0
|
|
||||||
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.fillWidth: true
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Content")
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: ruleContentArea.text.length > 0
|
|
||||||
onClicked: utils.copyToClipboard(ruleContentArea.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: ruleContentArea
|
|
||||||
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
color: palette.text
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
|
|
||||||
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property alias toastTextItem: textItem
|
|
||||||
property alias toastTextColor: textItem.color
|
|
||||||
|
|
||||||
property string errorText: ""
|
|
||||||
property int displayDuration: 7000
|
|
||||||
|
|
||||||
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
|
|
||||||
height: visible ? (textItem.implicitHeight + 12) : 0
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 10
|
|
||||||
|
|
||||||
color: "#d32f2f"
|
|
||||||
radius: height / 2
|
|
||||||
border.color: "#b71c1c"
|
|
||||||
border.width: 1
|
|
||||||
visible: false
|
|
||||||
opacity: 0
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: textItem
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.margins: 6
|
|
||||||
text: root.errorText
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 13
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
width: Math.min(implicitWidth, root.parent.width - 60)
|
|
||||||
horizontalAlignment: TextEdit.AlignHCenter
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
selectByKeyboard: true
|
|
||||||
selectionColor: "#b71c1c"
|
|
||||||
}
|
|
||||||
|
|
||||||
function show(message) {
|
|
||||||
errorText = message
|
|
||||||
visible = true
|
|
||||||
showAnimation.start()
|
|
||||||
hideTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
hideAnimation.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: showAnimation
|
|
||||||
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
id: hideAnimation
|
|
||||||
|
|
||||||
target: root
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InQuad
|
|
||||||
onFinished: root.visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: hideTimer
|
|
||||||
|
|
||||||
interval: root.displayDuration
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
onTriggered: root.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,9 +19,7 @@
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
@@ -32,212 +30,59 @@ Rectangle {
|
|||||||
property alias tokensBadge: tokensBadgeId
|
property alias tokensBadge: tokensBadgeId
|
||||||
property alias recentPath: recentPathId
|
property alias recentPath: recentPathId
|
||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
property alias pinButton: pinButtonId
|
|
||||||
property alias rulesButton: rulesButtonId
|
|
||||||
property alias agentModeSwitch: agentModeSwitchId
|
|
||||||
property alias thinkingMode: thinkingModeId
|
|
||||||
property alias activeRulesCount: activeRulesCountId.text
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
Qt.lighter(palette.window, 1.1)
|
Qt.lighter(palette.window, 1.1)
|
||||||
|
|
||||||
Flow {
|
RowLayout {
|
||||||
anchors {
|
anchors {
|
||||||
left: parent.left
|
left: parent.left
|
||||||
|
leftMargin: 5
|
||||||
right: parent.right
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
verticalCenter: parent.verticalCenter
|
verticalCenter: parent.verticalCenter
|
||||||
margins: 5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
Row {
|
QoAButton {
|
||||||
height: agentModeSwitchId.height
|
id: saveButtonId
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
QoAButton {
|
text: qsTr("Save")
|
||||||
id: pinButtonId
|
}
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
QoAButton {
|
||||||
checkable: true
|
id: loadButtonId
|
||||||
|
|
||||||
icon {
|
text: qsTr("Load")
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoATextSlider {
|
QoAButton {
|
||||||
id: agentModeSwitchId
|
id: clearButtonId
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
text: qsTr("Clear")
|
||||||
|
}
|
||||||
|
|
||||||
leftText: "chat"
|
Text {
|
||||||
rightText: "AI Agent"
|
id: recentPathId
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
elide: Text.ElideMiddle
|
||||||
ToolTip.delay: 250
|
color: palette.text
|
||||||
ToolTip.text: {
|
}
|
||||||
if (!agentModeSwitchId.enabled) {
|
|
||||||
return qsTr("Tools are disabled in General Settings")
|
|
||||||
}
|
|
||||||
return checked
|
|
||||||
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
|
|
||||||
: qsTr("Chat Mode: Simple conversation without tool access")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: thinkingModeId
|
id: openChatHistoryId
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
text: qsTr("Show in system")
|
||||||
|
|
||||||
checkable: true
|
|
||||||
opacity: enabled ? 1.0 : 0.2
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
|
||||||
: qsTr("Thinking Mode disabled"))
|
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
height: agentModeSwitchId.height
|
Layout.fillWidth: true
|
||||||
width: recentPathId.width
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: recentPathId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: Math.min(implicitWidth, root.width)
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 12
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
|
||||||
ToolTip.delay: 500
|
|
||||||
ToolTip.text: recentPathId.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Badge {
|
||||||
Layout.preferredWidth: root.width
|
id: tokensBadgeId
|
||||||
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: saveButtonId
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: openChatHistoryId
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: rulesButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
text: " "
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: root.activeRulesCount > 0
|
|
||||||
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
|
|
||||||
: qsTr("View active project rules (no rules found)")
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: activeRulesCountId
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottom: parent.bottom
|
|
||||||
bottomMargin: 2
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Badge {
|
|
||||||
id: tokensBadgeId
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
213
CodeHandler.cpp
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,177 +18,41 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
struct LanguageProperties
|
QString CodeHandler::processText(QString text)
|
||||||
{
|
|
||||||
QString name;
|
|
||||||
QString commentStyle;
|
|
||||||
QVector<QString> namesFromModel;
|
|
||||||
QVector<QString> fileExtensions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const QVector<LanguageProperties> customLanguagesFromSettings()
|
|
||||||
{
|
|
||||||
QVector<LanguageProperties> customLanguages;
|
|
||||||
|
|
||||||
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
|
|
||||||
for (const QString &entry : customLanguagesList) {
|
|
||||||
if (entry.trimmed().isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList parts = entry.split(',');
|
|
||||||
if (parts.size() < 4) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString name = parts[0].trimmed();
|
|
||||||
QString commentStyle = parts[1].trimmed();
|
|
||||||
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
|
|
||||||
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
|
|
||||||
|
|
||||||
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
|
|
||||||
&& !extensionsList.isEmpty()) {
|
|
||||||
QVector<QString> modelNames;
|
|
||||||
for (const auto &modelName : modelNamesList) {
|
|
||||||
modelNames.append(modelName);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<QString> extensions;
|
|
||||||
for (const auto &ext : extensionsList) {
|
|
||||||
extensions.append(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
customLanguages.append({name, commentStyle, modelNames, extensions});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return customLanguages;
|
|
||||||
}
|
|
||||||
const QVector<LanguageProperties> &getKnownLanguages()
|
|
||||||
{
|
|
||||||
static QVector<LanguageProperties> knownLanguages = {
|
|
||||||
{"python", "#", {"python", "py"}, {"py"}},
|
|
||||||
{"lua", "--", {"lua"}, {"lua"}},
|
|
||||||
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
|
|
||||||
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
|
|
||||||
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
|
|
||||||
{"java", "//", {"java"}, {"java"}},
|
|
||||||
{"c#", "//", {"cs", "csharp"}, {"cs"}},
|
|
||||||
{"php", "//", {"php"}, {"php"}},
|
|
||||||
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
|
|
||||||
{"go", "//", {"go"}, {"go"}},
|
|
||||||
{"swift", "//", {"swift"}, {"swift"}},
|
|
||||||
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
|
|
||||||
{"scala", "//", {"scala"}, {"scala"}},
|
|
||||||
{"r", "#", {"r"}, {"r"}},
|
|
||||||
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
|
|
||||||
{"perl", "#", {"pl", "perl"}, {"pl"}},
|
|
||||||
{"hs", "--", {"hs", "haskell"}, {"hs"}},
|
|
||||||
{"qml", "//", {"qml"}, {"qml"}},
|
|
||||||
};
|
|
||||||
|
|
||||||
knownLanguages.append(customLanguagesFromSettings());
|
|
||||||
|
|
||||||
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;
|
|
||||||
for (const auto &languageProps : getKnownLanguages()) {
|
|
||||||
result[languageProps.name] = languageProps.commentStyle;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static QHash<QString, QString> buildExtensionToLanguageMap()
|
|
||||||
{
|
|
||||||
QHash<QString, QString> result;
|
|
||||||
for (const auto &languageProps : getKnownLanguages()) {
|
|
||||||
for (const auto &extension : languageProps.fileExtensions) {
|
|
||||||
result[extension] = languageProps.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
|
|
||||||
{
|
|
||||||
QHash<QString, QString> result;
|
|
||||||
for (const auto &languageProps : getKnownLanguages()) {
|
|
||||||
for (const auto &nameFromModel : languageProps.namesFromModel) {
|
|
||||||
result[nameFromModel] = languageProps.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString CodeHandler::processText(QString text, QString currentFilePath)
|
|
||||||
{
|
{
|
||||||
QString result;
|
QString result;
|
||||||
QStringList lines = text.split('\n');
|
QStringList lines = text.split('\n');
|
||||||
bool inCodeBlock = false;
|
bool inCodeBlock = false;
|
||||||
QString pendingComments;
|
QString pendingComments;
|
||||||
|
QString currentLanguage;
|
||||||
auto currentFileExtension = QFileInfo(currentFilePath).suffix();
|
|
||||||
auto currentLanguage = detectLanguageFromExtension(currentFileExtension);
|
|
||||||
|
|
||||||
auto addPendingCommentsIfAny = [&]() {
|
|
||||||
if (pendingComments.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QStringList commentLines = pendingComments.split('\n');
|
|
||||||
QString commentPrefix = getCommentPrefix(currentLanguage);
|
|
||||||
|
|
||||||
for (const QString &commentLine : commentLines) {
|
|
||||||
if (!commentLine.trimmed().isEmpty()) {
|
|
||||||
result += commentPrefix + " " + commentLine.trimmed() + "\n";
|
|
||||||
} else {
|
|
||||||
result += "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingComments.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const QString &line : lines) {
|
for (const QString &line : lines) {
|
||||||
if (line.trimmed().startsWith("```")) {
|
if (line.trimmed().startsWith("```")) {
|
||||||
if (!inCodeBlock) {
|
if (!inCodeBlock) {
|
||||||
auto lineLanguage = detectLanguageFromLine(line);
|
currentLanguage = detectLanguage(line);
|
||||||
if (!lineLanguage.isEmpty()) {
|
|
||||||
currentLanguage = lineLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
addPendingCommentsIfAny();
|
|
||||||
|
|
||||||
if (lineLanguage.isEmpty()) {
|
|
||||||
// language not detected, so add direct output from model, if any
|
|
||||||
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inCodeBlock = !inCodeBlock;
|
inCodeBlock = !inCodeBlock;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inCodeBlock) {
|
if (inCodeBlock) {
|
||||||
|
if (!pendingComments.isEmpty()) {
|
||||||
|
QStringList commentLines = pendingComments.split('\n');
|
||||||
|
QString commentPrefix = getCommentPrefix(currentLanguage);
|
||||||
|
|
||||||
|
for (const QString &commentLine : commentLines) {
|
||||||
|
if (!commentLine.trimmed().isEmpty()) {
|
||||||
|
result += commentPrefix + " " + commentLine.trimmed() + "\n";
|
||||||
|
} else {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingComments.clear();
|
||||||
|
}
|
||||||
result += line + "\n";
|
result += line + "\n";
|
||||||
} else {
|
} else {
|
||||||
QString trimmed = line.trimmed();
|
QString trimmed = line.trimmed();
|
||||||
@@ -201,27 +64,45 @@ QString CodeHandler::processText(QString text, QString currentFilePath)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addPendingCommentsIfAny();
|
if (!pendingComments.isEmpty()) {
|
||||||
|
QStringList commentLines = pendingComments.split('\n');
|
||||||
|
QString commentPrefix = getCommentPrefix(currentLanguage);
|
||||||
|
|
||||||
|
for (const QString &commentLine : commentLines) {
|
||||||
|
if (!commentLine.trimmed().isEmpty()) {
|
||||||
|
result += commentPrefix + " " + commentLine.trimmed() + "\n";
|
||||||
|
} else {
|
||||||
|
result += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CodeHandler::getCommentPrefix(const QString &language)
|
QString CodeHandler::getCommentPrefix(const QString &language)
|
||||||
{
|
{
|
||||||
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
|
static const QHash<QString, QString> commentPrefixes
|
||||||
return commentPrefixes.value(language, "//");
|
= {{"python", "#"}, {"py", "#"}, {"lua", "--"}, {"javascript", "//"},
|
||||||
|
{"js", "//"}, {"typescript", "//"}, {"ts", "//"}, {"cpp", "//"},
|
||||||
|
{"c++", "//"}, {"c", "//"}, {"java", "//"}, {"csharp", "//"},
|
||||||
|
{"cs", "//"}, {"php", "//"}, {"ruby", "#"}, {"rb", "#"},
|
||||||
|
{"rust", "//"}, {"rs", "//"}, {"go", "//"}, {"swift", "//"},
|
||||||
|
{"kotlin", "//"}, {"kt", "//"}, {"scala", "//"}, {"r", "#"},
|
||||||
|
{"shell", "#"}, {"bash", "#"}, {"sh", "#"}, {"perl", "#"},
|
||||||
|
{"pl", "#"}, {"haskell", "--"}, {"hs", "--"}};
|
||||||
|
|
||||||
|
return commentPrefixes.value(language.toLower(), "//");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CodeHandler::detectLanguageFromLine(const QString &line)
|
QString CodeHandler::detectLanguage(const QString &line)
|
||||||
{
|
{
|
||||||
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
|
QString trimmed = line.trimmed();
|
||||||
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
|
if (trimmed.length() <= 3) { // Если только ```
|
||||||
}
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
|
return trimmed.mid(3).trimmed();
|
||||||
{
|
|
||||||
static const auto extensionToLanguage = buildExtensionToLanguageMap();
|
|
||||||
return extensionToLanguage.value(extension.toLower(), "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -28,25 +28,11 @@ namespace QodeAssist {
|
|||||||
class CodeHandler
|
class CodeHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static QString processText(QString text, QString currentFileName);
|
static QString processText(QString text);
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects language from line, or returns empty string if this was not possible
|
|
||||||
*/
|
|
||||||
static QString detectLanguageFromLine(const QString &line);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects language file name, or returns empty string if this was not possible
|
|
||||||
*/
|
|
||||||
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:
|
private:
|
||||||
static QString getCommentPrefix(const QString &language);
|
static QString getCommentPrefix(const QString &language);
|
||||||
|
static QString detectLanguage(const QString &line);
|
||||||
|
|
||||||
static const QRegularExpression &getFullCodeBlockRegex();
|
static const QRegularExpression &getFullCodeBlockRegex();
|
||||||
static const QRegularExpression &getPartialStartBlockRegex();
|
static const QRegularExpression &getPartialStartBlockRegex();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
|
|
||||||
#include <settings/ButtonAspect.hpp>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <settings/ButtonAspect.hpp>
|
||||||
|
|
||||||
#include "QodeAssisttr.h"
|
#include "QodeAssisttr.h"
|
||||||
|
|
||||||
@@ -35,52 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
|
|||||||
void ConfigurationManager::init()
|
void ConfigurationManager::init()
|
||||||
{
|
{
|
||||||
setupConnections();
|
setupConnections();
|
||||||
updateAllTemplateDescriptions();
|
|
||||||
checkAllTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (!templ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
|
||||||
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
|
||||||
{
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (templ->name() == templateAspect.value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplate.setValue(templ->name());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplate.setValue(templ->name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkAllTemplate()
|
|
||||||
{
|
|
||||||
checkTemplate(m_generalSettings.ccTemplate);
|
|
||||||
checkTemplate(m_generalSettings.caTemplate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||||
@@ -97,16 +51,12 @@ void ConfigurationManager::setupConnections()
|
|||||||
|
|
||||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||||
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||||
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||||
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||||
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||||
@@ -114,18 +64,6 @@ void ConfigurationManager::setupConnections()
|
|||||||
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
||||||
connect(
|
connect(
|
||||||
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||||
|
|
||||||
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
void ConfigurationManager::selectProvider()
|
||||||
@@ -140,13 +78,13 @@ void ConfigurationManager::selectProvider()
|
|||||||
? m_generalSettings.ccProvider
|
? m_generalSettings.ccProvider
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
||||||
? m_generalSettings.ccPreset1Provider
|
? m_generalSettings.ccPreset1Provider
|
||||||
: settingsButton == &m_generalSettings.qrSelectProvider
|
|
||||||
? m_generalSettings.qrProvider
|
|
||||||
: m_generalSettings.caProvider;
|
: m_generalSettings.caProvider;
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
||||||
m_generalSettings.showSelectionDialog(
|
m_generalSettings.showSelectionDialog(providersList,
|
||||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
targetSettings,
|
||||||
|
Tr::tr("Select LLM Provider"),
|
||||||
|
Tr::tr("Providers:"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,21 +96,17 @@ void ConfigurationManager::selectModel()
|
|||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
|
|
||||||
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
: m_generalSettings.caProvider.volatileValue();
|
||||||
|
|
||||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
||||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
|
||||||
: m_generalSettings.caUrl.volatileValue();
|
: m_generalSettings.caUrl.volatileValue();
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||||
: isQuickRefactor ? m_generalSettings.qrModel
|
|
||||||
: m_generalSettings.caModel;
|
: m_generalSettings.caModel;
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||||
@@ -203,25 +137,19 @@ void ConfigurationManager::selectTemplate()
|
|||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
|
||||||
|
|
||||||
const auto templateList = isCodeCompletion || isPreset1
|
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
|
||||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
: m_templateManger.chatTemplatesNames();
|
||||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
: isPreset1 ? m_generalSettings.ccPreset1Template
|
||||||
: isQuickRefactor ? m_generalSettings.qrTemplate
|
|
||||||
: m_generalSettings.caTemplate;
|
: m_generalSettings.caTemplate;
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||||
m_generalSettings.showSelectionDialog(
|
m_generalSettings.showSelectionDialog(templateList,
|
||||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
targetSettings,
|
||||||
|
Tr::tr("Select Template"),
|
||||||
|
Tr::tr("Templates:"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +169,6 @@ void ConfigurationManager::selectUrl()
|
|||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||||
? m_generalSettings.ccPreset1Url
|
? m_generalSettings.ccPreset1Url
|
||||||
: settingsButton == &m_generalSettings.qrSetUrl
|
|
||||||
? m_generalSettings.qrUrl
|
|
||||||
: m_generalSettings.caUrl;
|
: m_generalSettings.caUrl;
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -36,11 +36,6 @@ public:
|
|||||||
|
|
||||||
void init();
|
void init();
|
||||||
|
|
||||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
|
||||||
void updateAllTemplateDescriptions();
|
|
||||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
|
||||||
void checkAllTemplate();
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void selectProvider();
|
void selectProvider();
|
||||||
void selectModel();
|
void selectModel();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -23,42 +23,32 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <llmcore/RequestConfig.hpp>
|
||||||
|
#include <texteditor/textdocument.h>
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include "context/DocumentContextReader.hpp"
|
#include "context/DocumentContextReader.hpp"
|
||||||
#include "context/Utils.hpp"
|
#include "llmcore/MessageBuilder.hpp"
|
||||||
|
#include "llmcore/PromptTemplateManager.hpp"
|
||||||
|
#include "llmcore/ProvidersManager.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include <llmcore/RequestConfig.hpp>
|
|
||||||
#include <llmcore/RulesLoader.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
LLMClientInterface::LLMClientInterface(
|
LLMClientInterface::LLMClientInterface()
|
||||||
const Settings::GeneralSettings &generalSettings,
|
: m_requestHandler(this)
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
|
||||||
LLMCore::IProviderRegistry &providerRegistry,
|
|
||||||
LLMCore::IPromptProvider *promptProvider,
|
|
||||||
Context::IDocumentReader &documentReader,
|
|
||||||
IRequestPerformanceLogger &performanceLogger)
|
|
||||||
: m_generalSettings(generalSettings)
|
|
||||||
, m_completeSettings(completeSettings)
|
|
||||||
, m_providerRegistry(providerRegistry)
|
|
||||||
, m_promptProvider(promptProvider)
|
|
||||||
, m_documentReader(documentReader)
|
|
||||||
, m_performanceLogger(performanceLogger)
|
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
|
||||||
{
|
{
|
||||||
}
|
connect(&m_requestHandler,
|
||||||
|
&LLMCore::RequestHandler::completionReceived,
|
||||||
LLMClientInterface::~LLMClientInterface()
|
this,
|
||||||
{
|
&LLMClientInterface::sendCompletionToClient);
|
||||||
handleCancelRequest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||||
{
|
{
|
||||||
return "QodeAssist";
|
return "Qode Assist";
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::startImpl()
|
void LLMClientInterface::startImpl()
|
||||||
@@ -66,44 +56,6 @@ void LLMClientInterface::startImpl()
|
|||||||
emit started();
|
emit started();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
|
||||||
|
|
||||||
// Send LSP error response to client
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
QJsonObject response;
|
|
||||||
response["jsonrpc"] = "2.0";
|
|
||||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
|
||||||
|
|
||||||
QJsonObject errorObject;
|
|
||||||
errorObject["code"] = -32603; // Internal error code
|
|
||||||
errorObject["message"] = error;
|
|
||||||
response["error"] = errorObject;
|
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::sendData(const QByteArray &data)
|
void LLMClientInterface::sendData(const QByteArray &data)
|
||||||
{
|
{
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||||
@@ -123,11 +75,10 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
handleTextDocumentDidOpen(request);
|
handleTextDocumentDidOpen(request);
|
||||||
} else if (method == "getCompletionsCycling") {
|
} else if (method == "getCompletionsCycling") {
|
||||||
QString requestId = request["id"].toString();
|
QString requestId = request["id"].toString();
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
startTimeMeasurement(requestId);
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
qDebug() << "Cancelling request";
|
handleCancelRequest(request);
|
||||||
handleCancelRequest();
|
|
||||||
} else if (method == "exit") {
|
} else if (method == "exit") {
|
||||||
// TODO make exit handler
|
// TODO make exit handler
|
||||||
} else {
|
} else {
|
||||||
@@ -135,29 +86,14 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest()
|
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
QSet<LLMCore::Provider *> providers;
|
QString id = request["params"].toObject()["id"].toString();
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
if (m_requestHandler.cancelRequest(id)) {
|
||||||
if (it.value().provider) {
|
LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
|
||||||
providers.insert(it.value().provider);
|
} else {
|
||||||
}
|
LOG_MESSAGE(QString("Request %1 not found").arg(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeRequests.clear();
|
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleInitialize(const QJsonObject &request)
|
void LLMClientInterface::handleInitialize(const QJsonObject &request)
|
||||||
@@ -210,64 +146,46 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
|
bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
QJsonObject response;
|
auto &generalSettings = Settings::generalSettings();
|
||||||
response["jsonrpc"] = "2.0";
|
|
||||||
response[LanguageServerProtocol::idKey] = request["id"];
|
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
|
||||||
|
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
|
||||||
QJsonObject errorObject;
|
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
|
||||||
errorObject["code"] = -32603; // Internal error code
|
|
||||||
errorObject["message"] = errorMessage;
|
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
|
||||||
response["error"] = errorObject;
|
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
|
||||||
|
|
||||||
// End performance measurement if it was started
|
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
auto filePath = Context::extractFilePathFromRequest(request);
|
const auto updatedContext = prepareContext(request);
|
||||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
auto &completeSettings = Settings::codeCompletionSettings();
|
||||||
if (!documentInfo.document) {
|
auto &generalSettings = Settings::generalSettings();
|
||||||
QString error = QString("Document is not available: %1").arg(filePath);
|
|
||||||
LOG_MESSAGE("Error: " + error);
|
|
||||||
sendErrorResponse(request, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto updatedContext = prepareContext(request, documentInfo);
|
bool isPreset1Active = isSpecifyCompletion(request);
|
||||||
|
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
const auto providerName = !isPreset1Active ? generalSettings.ccProvider()
|
||||||
|
: generalSettings.ccPreset1Provider();
|
||||||
|
const auto modelName = !isPreset1Active ? generalSettings.ccModel()
|
||||||
|
: generalSettings.ccPreset1Model();
|
||||||
|
const auto url = !isPreset1Active ? generalSettings.ccUrl() : generalSettings.ccPreset1Url();
|
||||||
|
|
||||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
: m_generalSettings.ccPreset1Provider();
|
|
||||||
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
|
|
||||||
: m_generalSettings.ccPreset1Model();
|
|
||||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
|
||||||
: m_generalSettings.ccPreset1Url();
|
|
||||||
|
|
||||||
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||||
LOG_MESSAGE(error);
|
|
||||||
sendErrorResponse(request, error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
|
||||||
: m_generalSettings.ccPreset1Template();
|
: generalSettings.ccPreset1Template();
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
if (!promptTemplate) {
|
if (!promptTemplate) {
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||||
LOG_MESSAGE(error);
|
|
||||||
sendErrorResponse(request, error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,162 +194,107 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||||
config.provider = provider;
|
config.provider = provider;
|
||||||
config.promptTemplate = promptTemplate;
|
config.promptTemplate = promptTemplate;
|
||||||
// TODO refactor networking
|
config.url = QUrl(QString("%1%2").arg(
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
url,
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
|
||||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
: provider->chatEndpoint()));
|
||||||
} else {
|
|
||||||
config.url = QUrl(
|
|
||||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
|
||||||
config.providerRequest = {{"model", modelName}, {"stream", true}};
|
|
||||||
}
|
|
||||||
config.apiKey = provider->apiKey();
|
config.apiKey = provider->apiKey();
|
||||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
|
||||||
|
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
|
||||||
|
|
||||||
|
config.multiLineCompletion = completeSettings.multiLineCompletion();
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||||
if (!stopWords.isEmpty())
|
if (!stopWords.isEmpty())
|
||||||
config.providerRequest["stop"] = stopWords;
|
config.providerRequest["stop"] = stopWords;
|
||||||
|
|
||||||
QString systemPrompt;
|
QString systemPrompt;
|
||||||
if (m_completeSettings.useSystemPrompt())
|
if (completeSettings.useSystemPrompt())
|
||||||
systemPrompt.append(
|
systemPrompt.append(completeSettings.systemPrompt());
|
||||||
m_completeSettings.useUserMessageTemplateForCC()
|
if (!updatedContext.fileContext.isEmpty())
|
||||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
systemPrompt.append(updatedContext.fileContext);
|
||||||
? m_completeSettings.systemPromptForNonFimModels()
|
|
||||||
: m_completeSettings.systemPrompt());
|
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
QString userMessage;
|
||||||
if (project) {
|
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
||||||
QString projectRules
|
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
|
||||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
|
} else {
|
||||||
|
userMessage = updatedContext.prefix;
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for completion");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedContext.fileContext.has_value())
|
auto message = LLMCore::MessageBuilder()
|
||||||
systemPrompt.append(updatedContext.fileContext.value());
|
.addSystemMessage(systemPrompt)
|
||||||
|
.addUserMessage(userMessage)
|
||||||
|
.addSuffix(updatedContext.suffix)
|
||||||
|
.addTokenizer(promptTemplate);
|
||||||
|
|
||||||
if (m_completeSettings.useOpenFilesContext()) {
|
message.saveTo(
|
||||||
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
|
|
||||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
|
||||||
if (!updatedContext.filesMetadata) {
|
|
||||||
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
|
|
||||||
}
|
|
||||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContext.systemPrompt = systemPrompt;
|
|
||||||
|
|
||||||
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
|
||||||
QString userMessage;
|
|
||||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
|
||||||
userMessage = m_completeSettings.processMessageToFIM(
|
|
||||||
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
|
|
||||||
} else {
|
|
||||||
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO refactor add message
|
|
||||||
QVector<LLMCore::Message> messages;
|
|
||||||
messages.append({"user", userMessage});
|
|
||||||
updatedContext.history = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.provider->prepareRequest(
|
|
||||||
config.providerRequest,
|
config.providerRequest,
|
||||||
promptTemplate,
|
providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
|
||||||
updatedContext,
|
|
||||||
LLMCore::RequestType::CodeCompletion,
|
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
|
||||||
false,
|
|
||||||
false);
|
|
||||||
|
|
||||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
QString error = QString("Request validation failed: %1").arg(errors.join("; "));
|
LOG_MESSAGE("Validate errors for fim request:");
|
||||||
LOG_MESSAGE("Validate errors for request:");
|
|
||||||
LOG_MESSAGES(errors);
|
LOG_MESSAGES(errors);
|
||||||
sendErrorResponse(request, error);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_requestHandler.sendLLMRequest(config, request);
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::requestFailed,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
const QStringView &accumulatedCompletion)
|
||||||
{
|
{
|
||||||
QJsonObject params = request["params"].toObject();
|
QJsonObject params = request["params"].toObject();
|
||||||
QJsonObject doc = params["doc"].toObject();
|
QJsonObject doc = params["doc"].toObject();
|
||||||
QJsonObject position = doc["position"].toObject();
|
QJsonObject position = doc["position"].toObject();
|
||||||
|
QString uri = doc["uri"].toString();
|
||||||
|
|
||||||
|
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
|
||||||
|
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
|
||||||
|
filePath);
|
||||||
|
|
||||||
|
if (!textDocument) {
|
||||||
|
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
|
||||||
|
return LLMCore::ContextData{};
|
||||||
|
}
|
||||||
|
|
||||||
int cursorPosition = position["character"].toInt();
|
int cursorPosition = position["character"].toInt();
|
||||||
int lineNumber = position["line"].toInt();
|
int lineNumber = position["line"].toInt();
|
||||||
|
|
||||||
Context::DocumentContextReader
|
Context::DocumentContextReader reader(textDocument);
|
||||||
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
return reader.prepareContext(lineNumber, cursorPosition);
|
||||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString LLMClientInterface::endpoint(
|
Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
|
||||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
|
||||||
{
|
{
|
||||||
QString endpoint;
|
QJsonObject params = request["params"].toObject();
|
||||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
QJsonObject doc = params["doc"].toObject();
|
||||||
: m_generalSettings.ccEndpointMode.stringValue();
|
QString uri = doc["uri"].toString();
|
||||||
if (endpointMode == "Auto") {
|
|
||||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
|
||||||
: provider->chatEndpoint();
|
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
|
||||||
} else if (endpointMode == "Custom") {
|
filePath);
|
||||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
|
||||||
: m_generalSettings.ccCustomEndpoint();
|
if (!textDocument) {
|
||||||
} else if (endpointMode == "FIM") {
|
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
|
||||||
endpoint = provider->completionEndpoint();
|
return Context::ProgrammingLanguage::Unknown;
|
||||||
} else if (endpointMode == "Chat") {
|
|
||||||
endpoint = provider->chatEndpoint();
|
|
||||||
}
|
}
|
||||||
return endpoint;
|
|
||||||
|
return Context::ProgrammingLanguageUtils::fromMimeType(textDocument->mimeType());
|
||||||
}
|
}
|
||||||
|
|
||||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||||
|
const QJsonObject &request,
|
||||||
|
bool isComplete)
|
||||||
{
|
{
|
||||||
return m_contextManager;
|
bool isPreset1Active = isSpecifyCompletion(request);
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::sendCompletionToClient(
|
auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
|
||||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
: Settings::generalSettings().ccPreset1Template();
|
||||||
{
|
|
||||||
auto filePath = Context::extractFilePathFromRequest(request);
|
|
||||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
||||||
: m_generalSettings.ccPreset1Template();
|
templateName);
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
|
||||||
|
|
||||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||||
|
|
||||||
@@ -443,38 +306,18 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
QJsonArray completions;
|
QJsonArray completions;
|
||||||
QJsonObject completionItem;
|
QJsonObject completionItem;
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
|
QString processedCompletion
|
||||||
|
= promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||||
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
|
&& Settings::codeCompletionSettings().smartProcessInstuctText()
|
||||||
QString processedCompletion;
|
? CodeHandler::processText(completion)
|
||||||
|
: completion;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processedCompletion.endsWith('\n')) {
|
|
||||||
QString withoutTrailing = processedCompletion.chopped(1);
|
|
||||||
if (!withoutTrailing.contains('\n')) {
|
|
||||||
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
|
|
||||||
processedCompletion = withoutTrailing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||||
|
|
||||||
QJsonObject range;
|
QJsonObject range;
|
||||||
range["start"] = position;
|
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::rangeKey] = range;
|
||||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||||
completions.append(completionItem);
|
completions.append(completionItem);
|
||||||
@@ -486,13 +329,37 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
QString("Completions: \n%1")
|
QString("Completions: \n%1")
|
||||||
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
|
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(QString("Full response: \n%1")
|
||||||
QString("Full response: \n%1")
|
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
|
||||||
|
|
||||||
QString requestId = request["id"].toString();
|
QString requestId = request["id"].toString();
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
endTimeMeasurement(requestId);
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::startTimeMeasurement(const QString &requestId)
|
||||||
|
{
|
||||||
|
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::endTimeMeasurement(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (m_requestStartTimes.contains(requestId)) {
|
||||||
|
qint64 startTime = m_requestStartTimes[requestId];
|
||||||
|
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
qint64 totalTime = endTime - startTime;
|
||||||
|
logPerformance(requestId, "TotalCompletionTime", totalTime);
|
||||||
|
m_requestStartTimes.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::logPerformance(const QString &requestId,
|
||||||
|
const QString &operation,
|
||||||
|
qint64 elapsedMs)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::parseCurrentMessage() {}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -22,15 +22,9 @@
|
|||||||
#include <languageclient/languageclientinterface.h>
|
#include <languageclient/languageclientinterface.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include <context/ContextManager.hpp>
|
|
||||||
#include <context/IDocumentReader.hpp>
|
|
||||||
#include <context/ProgrammingLanguage.hpp>
|
#include <context/ProgrammingLanguage.hpp>
|
||||||
#include <llmcore/ContextData.hpp>
|
#include <llmcore/ContextData.hpp>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
#include <llmcore/RequestHandler.hpp>
|
||||||
#include <llmcore/IProviderRegistry.hpp>
|
|
||||||
#include <logger/IRequestPerformanceLogger.hpp>
|
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
|
||||||
#include <settings/GeneralSettings.hpp>
|
|
||||||
|
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
class QNetworkAccessManager;
|
class QNetworkAccessManager;
|
||||||
@@ -42,33 +36,20 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LLMClientInterface(
|
LLMClientInterface();
|
||||||
const Settings::GeneralSettings &generalSettings,
|
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
|
||||||
LLMCore::IProviderRegistry &providerRegistry,
|
|
||||||
LLMCore::IPromptProvider *promptProvider,
|
|
||||||
Context::IDocumentReader &documentReader,
|
|
||||||
IRequestPerformanceLogger &performanceLogger);
|
|
||||||
~LLMClientInterface() override;
|
|
||||||
|
|
||||||
Utils::FilePath serverDeviceTemplate() const override;
|
Utils::FilePath serverDeviceTemplate() const override;
|
||||||
|
|
||||||
void sendCompletionToClient(
|
void sendCompletionToClient(const QString &completion,
|
||||||
const QString &completion, const QJsonObject &request, bool isComplete);
|
const QJsonObject &request,
|
||||||
|
bool isComplete);
|
||||||
|
|
||||||
void handleCompletion(const QJsonObject &request);
|
void handleCompletion(const QJsonObject &request);
|
||||||
|
|
||||||
// exposed for tests
|
|
||||||
void sendData(const QByteArray &data) override;
|
|
||||||
|
|
||||||
Context::ContextManager *contextManager() const;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void startImpl() override;
|
void startImpl() override;
|
||||||
|
void sendData(const QByteArray &data) override;
|
||||||
private slots:
|
void parseCurrentMessage() override;
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleInitialize(const QJsonObject &request);
|
void handleInitialize(const QJsonObject &request);
|
||||||
@@ -76,28 +57,20 @@ private:
|
|||||||
void handleTextDocumentDidOpen(const QJsonObject &request);
|
void handleTextDocumentDidOpen(const QJsonObject &request);
|
||||||
void handleInitialized(const QJsonObject &request);
|
void handleInitialized(const QJsonObject &request);
|
||||||
void handleExit(const QJsonObject &request);
|
void handleExit(const QJsonObject &request);
|
||||||
void handleCancelRequest();
|
void handleCancelRequest(const QJsonObject &request);
|
||||||
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
|
||||||
|
|
||||||
struct RequestContext
|
|
||||||
{
|
|
||||||
QJsonObject originalRequest;
|
|
||||||
LLMCore::Provider *provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
LLMCore::ContextData prepareContext(
|
LLMCore::ContextData prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
|
||||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
|
||||||
|
bool isSpecifyCompletion(const QJsonObject &request);
|
||||||
|
|
||||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
LLMCore::RequestHandler m_requestHandler;
|
||||||
const Settings::GeneralSettings &m_generalSettings;
|
|
||||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
|
||||||
LLMCore::IProviderRegistry &m_providerRegistry;
|
|
||||||
Context::IDocumentReader &m_documentReader;
|
|
||||||
IRequestPerformanceLogger &m_performanceLogger;
|
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
Context::ContextManager *m_contextManager;
|
QMap<QString, qint64> m_requestStartTimes;
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
|
||||||
|
void startTimeMeasurement(const QString &requestId);
|
||||||
|
void endTimeMeasurement(const QString &requestId);
|
||||||
|
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -29,59 +29,6 @@
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
static QStringList extractTokens(const QString &str)
|
|
||||||
{
|
|
||||||
QStringList tokens;
|
|
||||||
QString currentToken;
|
|
||||||
for (const QChar &ch : str) {
|
|
||||||
if (ch.isLetterOrNumber() || ch == '_') {
|
|
||||||
currentToken += ch;
|
|
||||||
} else {
|
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
currentToken.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
|
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine)
|
|
||||||
{
|
|
||||||
if (rightText.isEmpty()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString structuralChars = "{}[]()<>;,";
|
|
||||||
bool hasStructuralOverlap = false;
|
|
||||||
for (const QChar &ch : structuralChars) {
|
|
||||||
if (suggestion.contains(ch) && rightText.contains(ch)) {
|
|
||||||
hasStructuralOverlap = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStructuralOverlap) {
|
|
||||||
return rightText.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList suggestionTokens = extractTokens(suggestion);
|
|
||||||
const QStringList lineTokens = extractTokens(entireLine);
|
|
||||||
|
|
||||||
for (const auto &token : suggestionTokens) {
|
|
||||||
if (lineTokens.contains(token)) {
|
|
||||||
return rightText.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
LLMSuggestion::LLMSuggestion(
|
LLMSuggestion::LLMSuggestion(
|
||||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||||
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||||
@@ -89,38 +36,23 @@ LLMSuggestion::LLMSuggestion(
|
|||||||
const auto &data = suggestions[currentCompletion];
|
const auto &data = suggestions[currentCompletion];
|
||||||
|
|
||||||
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||||
|
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||||
|
|
||||||
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
|
||||||
|
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
|
||||||
|
|
||||||
QTextCursor cursor(sourceDocument);
|
QTextCursor cursor(sourceDocument);
|
||||||
cursor.setPosition(startPos);
|
cursor.setPosition(startPos);
|
||||||
|
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
QTextBlock block = cursor.block();
|
QTextBlock block = cursor.block();
|
||||||
QString blockText = block.text();
|
QString blockText = block.text();
|
||||||
|
|
||||||
int cursorPositionInBlock = cursor.positionInBlock();
|
int startPosInBlock = startPos - block.position();
|
||||||
QString leftText = blockText.left(cursorPositionInBlock);
|
int endPosInBlock = endPos - block.position();
|
||||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
|
||||||
|
|
||||||
QString suggestionText = data.text;
|
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
|
||||||
QString entireLine = blockText;
|
replacementDocument()->setPlainText(blockText);
|
||||||
|
|
||||||
if (!suggestionText.contains('\n')) {
|
|
||||||
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
|
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
|
||||||
|
|
||||||
QString displayText = leftText + suggestionText + remainingRightText;
|
|
||||||
replacementDocument()->setPlainText(displayText);
|
|
||||||
} else {
|
|
||||||
int firstLineEnd = suggestionText.indexOf('\n');
|
|
||||||
QString firstLine = suggestionText.left(firstLineEnd);
|
|
||||||
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
|
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
|
||||||
|
|
||||||
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
|
||||||
replacementDocument()->setPlainText(displayText);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
||||||
@@ -135,125 +67,41 @@ bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
|||||||
|
|
||||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
const auto ¤tSuggestions = suggestions();
|
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||||
const auto ¤tData = currentSuggestions[currentSuggestion()];
|
|
||||||
const Utils::Text::Range range = currentData.range;
|
|
||||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||||
QTextCursor currentCursor = widget->textCursor();
|
QTextCursor currentCursor = widget->textCursor();
|
||||||
const QString text = currentData.text;
|
const QString text = suggestions()[currentSuggestion()].text;
|
||||||
|
|
||||||
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||||
+ (cursor.selectionEnd() - cursor.selectionStart());
|
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||||
|
|
||||||
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||||
|
|
||||||
if (next == -1) {
|
if (next == -1)
|
||||||
if (part == Line) {
|
return apply();
|
||||||
next = text.length();
|
|
||||||
} else {
|
|
||||||
return apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part == Line)
|
if (part == Line)
|
||||||
++next;
|
++next;
|
||||||
|
|
||||||
QString subText = text.mid(startPos, next - startPos);
|
QString subText = text.mid(startPos, next - startPos);
|
||||||
|
if (subText.isEmpty())
|
||||||
if (subText.isEmpty()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (startPos == 0) {
|
currentCursor.insertText(subText);
|
||||||
QTextBlock currentBlock = cursor.block();
|
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
|
||||||
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
|
||||||
currentCursor.removeSelectedText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subText.contains('\n')) {
|
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||||
currentCursor.insertText(subText);
|
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||||
|
if (!newCompletionText.isEmpty()) {
|
||||||
const QString remainingText = text.mid(next);
|
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||||
if (!remainingText.isEmpty()) {
|
|
||||||
QTextCursor newCursor = widget->textCursor();
|
|
||||||
const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument(
|
|
||||||
newCursor.document(), newCursor.position());
|
|
||||||
const Utils::Text::Position
|
const Utils::Text::Position
|
||||||
newEnd{newStart.line, newStart.column + int(remainingText.length())};
|
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
|
||||||
const Utils::Text::Range newRange{newStart, newEnd};
|
const Utils::Text::Range newRange{newStart, newEnd};
|
||||||
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
|
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
|
||||||
widget->insertSuggestion(
|
widget->insertSuggestion(
|
||||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LLMSuggestion::apply()
|
|
||||||
{
|
|
||||||
const auto ¤tSuggestions = suggestions();
|
|
||||||
const auto ¤tData = currentSuggestions[currentSuggestion()];
|
|
||||||
const Utils::Text::Range range = currentData.range;
|
|
||||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
|
||||||
QString text = currentData.text;
|
|
||||||
|
|
||||||
QTextBlock currentBlock = cursor.block();
|
|
||||||
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
|
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
|
||||||
QTextCursor editCursor = cursor;
|
|
||||||
editCursor.beginEditBlock();
|
|
||||||
|
|
||||||
int firstLineEnd = text.indexOf('\n');
|
|
||||||
if (firstLineEnd != -1) {
|
|
||||||
QString firstLine = text.left(firstLineEnd);
|
|
||||||
QString restOfText = text.mid(firstLineEnd);
|
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
|
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
|
||||||
editCursor.removeSelectedText();
|
|
||||||
}
|
|
||||||
|
|
||||||
editCursor.insertText(firstLine + restOfText);
|
|
||||||
} else {
|
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
|
||||||
editCursor.removeSelectedText();
|
|
||||||
}
|
|
||||||
|
|
||||||
editCursor.insertText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
editCursor.endEditBlock();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -40,10 +40,5 @@ public:
|
|||||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||||
bool apply() override;
|
|
||||||
|
|
||||||
static int calculateReplaceLength(const QString &suggestion,
|
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine);
|
|
||||||
};
|
};
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -68,10 +68,9 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
|
|||||||
public:
|
public:
|
||||||
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
||||||
|
|
||||||
GetCompletionParams(
|
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||||
const LanguageServerProtocol::TextDocumentIdentifier &document,
|
int version,
|
||||||
int version,
|
const LanguageServerProtocol::Position &position)
|
||||||
const LanguageServerProtocol::Position &position)
|
|
||||||
{
|
{
|
||||||
setTextDocument(document);
|
setTextDocument(document);
|
||||||
setVersion(version);
|
setVersion(version);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.0",
|
"Version" : "0.4.13",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||||
"License" : "GPLv3",
|
"License" : "GPLv3",
|
||||||
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
||||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
"DocumentationUrl" : "",
|
||||||
${IDE_PLUGIN_DEPENDENCIES}
|
${IDE_PLUGIN_DEPENDENCIES}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,5 @@
|
|||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>resources/images/qoderassist-icon@2x.png</file>
|
<file>resources/images/qoderassist-icon@2x.png</file>
|
||||||
<file>resources/images/qoderassist-icon.png</file>
|
<file>resources/images/qoderassist-icon.png</file>
|
||||||
<file>resources/images/repeat-last-instruct-icon@2x.png</file>
|
|
||||||
<file>resources/images/repeat-last-instruct-icon.png</file>
|
|
||||||
<file>resources/images/improve-current-code-icon@2x.png</file>
|
|
||||||
<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>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of Qode Assist.
|
||||||
*
|
*
|
||||||
* The Qt Company portions:
|
* The Qt Company portions:
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
@@ -24,24 +24,17 @@
|
|||||||
|
|
||||||
#include "QodeAssistClient.hpp"
|
#include "QodeAssistClient.hpp"
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QKeyEvent>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include <coreplugin/icore.h>
|
|
||||||
#include <languageclient/languageclientsettings.h>
|
#include <languageclient/languageclientsettings.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
#include "RefactorSuggestion.hpp"
|
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProjectSettings.hpp"
|
#include "settings/ProjectSettings.hpp"
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <logger/Logger.hpp>
|
|
||||||
|
|
||||||
using namespace LanguageServerProtocol;
|
using namespace LanguageServerProtocol;
|
||||||
using namespace TextEditor;
|
using namespace TextEditor;
|
||||||
@@ -51,12 +44,11 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
QodeAssistClient::QodeAssistClient()
|
||||||
: LanguageClient::Client(clientInterface)
|
: LanguageClient::Client(new LLMClientInterface())
|
||||||
, m_llmClient(clientInterface)
|
|
||||||
, m_recentCharCount(0)
|
, m_recentCharCount(0)
|
||||||
{
|
{
|
||||||
setName("QodeAssist");
|
setName("Qode Assist");
|
||||||
LanguageClient::LanguageFilter filter;
|
LanguageClient::LanguageFilter filter;
|
||||||
filter.mimeTypes = QStringList() << "*";
|
filter.mimeTypes = QStringList() << "*";
|
||||||
setSupportedLanguage(filter);
|
setSupportedLanguage(filter);
|
||||||
@@ -65,14 +57,11 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
setupConnections();
|
setupConnections();
|
||||||
|
|
||||||
m_typingTimer.start();
|
m_typingTimer.start();
|
||||||
|
|
||||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
delete m_refactorHoverHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||||
@@ -82,14 +71,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Client::openDocument(document);
|
Client::openDocument(document);
|
||||||
|
|
||||||
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
|
|
||||||
for (auto *editor : editors) {
|
|
||||||
if (auto *widget = editor->editorWidget()) {
|
|
||||||
widget->addHoverHandler(m_refactorHoverHandler);
|
|
||||||
widget->installEventFilter(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connect(
|
connect(
|
||||||
document,
|
document,
|
||||||
&TextDocument::contentsChangedWithPosition,
|
&TextDocument::contentsChangedWithPosition,
|
||||||
@@ -147,13 +128,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
scheduleRequest(widget);
|
scheduleRequest(widget);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// auto editors = BaseTextEditor::textEditorsForDocument(document);
|
|
||||||
// connect(
|
|
||||||
// editors.first()->editorWidget(),
|
|
||||||
// &TextEditorWidget::selectionChanged,
|
|
||||||
// this,
|
|
||||||
// [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
|
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
|
||||||
@@ -168,32 +142,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MultiTextCursor cursor = editor->multiTextCursor();
|
MultiTextCursor cursor = editor->multiTextCursor();
|
||||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const FilePath filePath = editor->textDocument()->filePath();
|
const FilePath filePath = editor->textDocument()->filePath();
|
||||||
GetCompletionRequest request{
|
GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||||
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
documentVersion(filePath),
|
||||||
documentVersion(filePath),
|
Position(cursor.mainCursor())}};
|
||||||
Position(cursor.mainCursor())}};
|
|
||||||
if (Settings::codeCompletionSettings().showProgressWidget()) {
|
|
||||||
// Setup cancel callback before showing progress
|
|
||||||
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
|
|
||||||
if (editor) {
|
|
||||||
cancelRunningRequest(editor);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
m_progressHandler.showProgress(editor);
|
|
||||||
}
|
|
||||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||||
const GetCompletionRequest::Response &response) {
|
const GetCompletionRequest::Response &response) {
|
||||||
QTC_ASSERT(editor, return);
|
QTC_ASSERT(editor, return);
|
||||||
@@ -203,42 +159,6 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
sendMessage(request);
|
sendMessage(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::requestQuickRefactor(
|
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
|
||||||
{
|
|
||||||
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
|
|
||||||
|
|
||||||
if (!isEnabled(project))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_refactorHandler) {
|
|
||||||
m_refactorHandler = new QuickRefactorHandler(this);
|
|
||||||
connect(
|
|
||||||
m_refactorHandler,
|
|
||||||
&QuickRefactorHandler::refactoringCompleted,
|
|
||||||
this,
|
|
||||||
&QodeAssistClient::handleRefactoringResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup cancel callback before showing progress
|
|
||||||
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
|
|
||||||
if (editor && m_refactorHandler) {
|
|
||||||
m_refactorHandler->cancelRequest();
|
|
||||||
m_progressHandler.hideProgress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
m_progressHandler.showProgress(editor);
|
|
||||||
m_refactorHandler->sendRefactorRequest(editor, instructions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
@@ -268,19 +188,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
|||||||
it.value()->setProperty("cursorPosition", editor->textCursor().position());
|
it.value()->setProperty("cursorPosition", editor->textCursor().position());
|
||||||
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
|
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
|
||||||
}
|
}
|
||||||
void QodeAssistClient::handleCompletions(
|
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
|
||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
if (response.error())
|
||||||
|
|
||||||
if (response.error()) {
|
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
|
|
||||||
QString errorMessage = tr("Code completion failed: %1").arg(response.error()->message());
|
|
||||||
m_errorHandler.showError(editor, errorMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int requestPosition = -1;
|
int requestPosition = -1;
|
||||||
if (const auto requestParams = m_runningRequests.take(editor).params())
|
if (const auto requestParams = m_runningRequests.take(editor).params())
|
||||||
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
||||||
@@ -323,11 +236,8 @@ void QodeAssistClient::handleCompletions(
|
|||||||
Text::Position pos{toTextPos(c.position())};
|
Text::Position pos{toTextPos(c.position())};
|
||||||
return TextSuggestion::Data{range, pos, c.text()};
|
return TextSuggestion::Data{range, pos, c.text()};
|
||||||
});
|
});
|
||||||
|
if (completions.isEmpty())
|
||||||
if (completions.isEmpty()) {
|
|
||||||
LOG_MESSAGE("No valid completions received");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +247,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
|||||||
const auto it = m_runningRequests.constFind(editor);
|
const auto it = m_runningRequests.constFind(editor);
|
||||||
if (it == m_runningRequests.constEnd())
|
if (it == m_runningRequests.constEnd())
|
||||||
return;
|
return;
|
||||||
m_progressHandler.hideProgress();
|
|
||||||
cancelRequest(it->id());
|
cancelRequest(it->id());
|
||||||
m_runningRequests.erase(it);
|
m_runningRequests.erase(it);
|
||||||
}
|
}
|
||||||
@@ -358,13 +267,18 @@ void QodeAssistClient::setupConnections()
|
|||||||
openDocument(textDocument);
|
openDocument(textDocument);
|
||||||
};
|
};
|
||||||
|
|
||||||
m_documentOpenedConnection
|
m_documentOpenedConnection = connect(EditorManager::instance(),
|
||||||
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
|
&EditorManager::documentOpened,
|
||||||
m_documentClosedConnection = connect(
|
this,
|
||||||
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
|
openDoc);
|
||||||
if (auto textDocument = qobject_cast<TextDocument *>(document))
|
m_documentClosedConnection = connect(EditorManager::instance(),
|
||||||
closeDocument(textDocument);
|
&EditorManager::documentClosed,
|
||||||
});
|
this,
|
||||||
|
[this](IDocument *document) {
|
||||||
|
if (auto textDocument = qobject_cast<TextDocument *>(
|
||||||
|
document))
|
||||||
|
closeDocument(textDocument);
|
||||||
|
});
|
||||||
|
|
||||||
for (IDocument *doc : DocumentModel::openedDocuments())
|
for (IDocument *doc : DocumentModel::openedDocuments())
|
||||||
openDoc(doc);
|
openDoc(doc);
|
||||||
@@ -379,118 +293,4 @@ void QodeAssistClient::cleanupConnections()
|
|||||||
m_scheduledRequests.clear();
|
m_scheduledRequests.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|
||||||
{
|
|
||||||
m_progressHandler.hideProgress();
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
// Show error to user
|
|
||||||
QString errorMessage = result.errorMessage.isEmpty()
|
|
||||||
? tr("Quick refactor failed")
|
|
||||||
: tr("Quick refactor failed: %1").arg(result.errorMessage);
|
|
||||||
|
|
||||||
if (result.editor) {
|
|
||||||
m_errorHandler.showError(result.editor, errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.editor) {
|
|
||||||
LOG_MESSAGE("Refactoring result has no editor");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
|
||||||
|
|
||||||
auto toTextPos = [](const Utils::Text::Position &pos) {
|
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
|
||||||
};
|
|
||||||
|
|
||||||
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
|
||||||
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
|
||||||
|
|
||||||
int startPos = range.begin.toPositionInDocument(editorWidget->document());
|
|
||||||
int endPos = range.end.toPositionInDocument(editorWidget->document());
|
|
||||||
|
|
||||||
if (startPos != endPos) {
|
|
||||||
QTextCursor startCursor(editorWidget->document());
|
|
||||||
startCursor.setPosition(startPos);
|
|
||||||
if (startCursor.positionInBlock() > 0) {
|
|
||||||
startCursor.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor endCursor(editorWidget->document());
|
|
||||||
endCursor.setPosition(endPos);
|
|
||||||
if (endCursor.positionInBlock() > 0) {
|
|
||||||
endCursor.movePosition(QTextCursor::EndOfBlock);
|
|
||||||
if (!endCursor.atEnd()) {
|
|
||||||
endCursor.movePosition(QTextCursor::NextCharacter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
|
|
||||||
editorWidget->document(), startCursor.position());
|
|
||||||
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
|
|
||||||
editorWidget->document(), endCursor.position());
|
|
||||||
|
|
||||||
range = Utils::Text::Range(expandedBegin, expandedEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditor::TextSuggestion::Data suggestionData{
|
|
||||||
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
|
|
||||||
pos,
|
|
||||||
result.newText};
|
|
||||||
editorWidget->insertSuggestion(
|
|
||||||
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
|
|
||||||
|
|
||||||
m_refactorHoverHandler->setSuggestionRange(range);
|
|
||||||
|
|
||||||
m_refactorHoverHandler->setApplyCallback([this, editorWidget]() {
|
|
||||||
QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier);
|
|
||||||
QApplication::sendEvent(editorWidget, &tabEvent);
|
|
||||||
m_refactorHoverHandler->clearSuggestionRange();
|
|
||||||
});
|
|
||||||
|
|
||||||
m_refactorHoverHandler->setDismissCallback([this, editorWidget]() {
|
|
||||||
editorWidget->clearSuggestion();
|
|
||||||
m_refactorHoverHandler->clearSuggestionRange();
|
|
||||||
});
|
|
||||||
|
|
||||||
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
|
|
||||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
|
||||||
|
|
||||||
if (keyEvent->key() == Qt::Key_Escape) {
|
|
||||||
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
|
|
||||||
|
|
||||||
if (editor) {
|
|
||||||
if (m_runningRequests.contains(editor)) {
|
|
||||||
cancelRunningRequest(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_scheduledRequests.contains(editor)) {
|
|
||||||
auto *timer = m_scheduledRequests.value(editor);
|
|
||||||
if (timer && timer->isActive()) {
|
|
||||||
timer->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_refactorHandler && m_refactorHandler->isProcessing()) {
|
|
||||||
m_refactorHandler->cancelRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_progressHandler.hideProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LanguageClient::Client::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of Qode Assist.
|
||||||
*
|
*
|
||||||
* The Qt Company portions:
|
* The Qt Company portions:
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||||
@@ -24,48 +24,32 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
|
||||||
#include "LSPCompletion.hpp"
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
|
||||||
#include "widgets/CompletionProgressHandler.hpp"
|
|
||||||
#include "widgets/CompletionErrorHandler.hpp"
|
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
|
||||||
#include <llmcore/IProviderRegistry.hpp>
|
#include "LSPCompletion.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
explicit QodeAssistClient();
|
||||||
~QodeAssistClient() override;
|
~QodeAssistClient() override;
|
||||||
|
|
||||||
void openDocument(TextEditor::TextDocument *document) override;
|
void openDocument(TextEditor::TextDocument *document) override;
|
||||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||||
|
|
||||||
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
||||||
void requestQuickRefactor(
|
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
||||||
void handleCompletions(
|
void handleCompletions(const GetCompletionRequest::Response &response,
|
||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
|
TextEditor::TextEditorWidget *editor);
|
||||||
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
||||||
bool isEnabled(ProjectExplorer::Project *project) const;
|
bool isEnabled(ProjectExplorer::Project *project) const;
|
||||||
|
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
void cleanupConnections();
|
void cleanupConnections();
|
||||||
void handleRefactoringResult(const RefactorResult &result);
|
|
||||||
|
|
||||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||||
@@ -74,12 +58,6 @@ private:
|
|||||||
|
|
||||||
QElapsedTimer m_typingTimer;
|
QElapsedTimer m_typingTimer;
|
||||||
int m_recentCharCount;
|
int m_recentCharCount;
|
||||||
CompletionProgressHandler m_progressHandler;
|
|
||||||
CompletionErrorHandler m_errorHandler;
|
|
||||||
EditorChatButtonHandler m_chatButtonHandler;
|
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
|
||||||
LLMClientInterface *m_llmClient;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
3
QodeAssist_en_001.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="en_001"></TS>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,415 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "QuickRefactorHandler.hpp"
|
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
#include <context/DocumentContextReader.hpp>
|
|
||||||
#include <context/DocumentReaderQtCreator.hpp>
|
|
||||||
#include <context/Utils.hpp>
|
|
||||||
#include <llmcore/PromptTemplateManager.hpp>
|
|
||||||
#include <llmcore/ProvidersManager.hpp>
|
|
||||||
#include <llmcore/RequestConfig.hpp>
|
|
||||||
#include <llmcore/RulesLoader.hpp>
|
|
||||||
#include <logger/Logger.hpp>
|
|
||||||
#include <settings/ChatAssistantSettings.hpp>
|
|
||||||
#include <settings/GeneralSettings.hpp>
|
|
||||||
#include <settings/QuickRefactorSettings.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_currentEditor(nullptr)
|
|
||||||
, m_isRefactoringInProgress(false)
|
|
||||||
, m_contextManager(this)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::sendRefactorRequest(
|
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
|
||||||
{
|
|
||||||
if (m_isRefactoringInProgress) {
|
|
||||||
cancelRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentEditor = editor;
|
|
||||||
|
|
||||||
Utils::Text::Range range;
|
|
||||||
if (editor->textCursor().hasSelection()) {
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
|
||||||
int startPos = cursor.selectionStart();
|
|
||||||
int endPos = cursor.selectionEnd();
|
|
||||||
|
|
||||||
QTextBlock startBlock = editor->document()->findBlock(startPos);
|
|
||||||
int startLine = startBlock.blockNumber() + 1;
|
|
||||||
int startColumn = startPos - startBlock.position();
|
|
||||||
|
|
||||||
QTextBlock endBlock = editor->document()->findBlock(endPos);
|
|
||||||
int endLine = endBlock.blockNumber() + 1;
|
|
||||||
int endColumn = endPos - endBlock.position();
|
|
||||||
|
|
||||||
Utils::Text::Position startPosition;
|
|
||||||
startPosition.line = startLine;
|
|
||||||
startPosition.column = startColumn;
|
|
||||||
|
|
||||||
Utils::Text::Position endPosition;
|
|
||||||
endPosition.line = endLine;
|
|
||||||
endPosition.column = endColumn;
|
|
||||||
|
|
||||||
range = Utils::Text::Range();
|
|
||||||
range.begin = startPosition;
|
|
||||||
range.end = endPosition;
|
|
||||||
} else {
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
|
||||||
int cursorPos = cursor.position();
|
|
||||||
|
|
||||||
QTextBlock block = editor->document()->findBlock(cursorPos);
|
|
||||||
int line = block.blockNumber() + 1;
|
|
||||||
int column = cursorPos - block.position();
|
|
||||||
|
|
||||||
Utils::Text::Position cursorPosition;
|
|
||||||
cursorPosition.line = line;
|
|
||||||
cursorPosition.column = column;
|
|
||||||
range = Utils::Text::Range();
|
|
||||||
range.begin = cursorPosition;
|
|
||||||
range.end = cursorPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentRange = range;
|
|
||||||
prepareAndSendRequest(editor, instructions, range);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::prepareAndSendRequest(
|
|
||||||
TextEditor::TextEditorWidget *editor,
|
|
||||||
const QString &instructions,
|
|
||||||
const Utils::Text::Range &range)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
auto &providerRegistry = LLMCore::ProvidersManager::instance();
|
|
||||||
auto &promptManager = LLMCore::PromptTemplateManager::instance();
|
|
||||||
|
|
||||||
const auto providerName = settings.qrProvider();
|
|
||||||
auto provider = providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
||||||
LOG_MESSAGE(error);
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
result.editor = editor;
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto templateName = settings.qrTemplate();
|
|
||||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
|
||||||
|
|
||||||
if (!promptTemplate) {
|
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
|
||||||
LOG_MESSAGE(error);
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
result.editor = editor;
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LLMCore::LLMConfig config;
|
|
||||||
config.requestType = LLMCore::RequestType::QuickRefactoring;
|
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
|
||||||
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
|
|
||||||
config.apiKey = provider->apiKey();
|
|
||||||
|
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
|
||||||
config.url = QUrl(QString("%1/models/%2:%3")
|
|
||||||
.arg(
|
|
||||||
Settings::generalSettings().qrUrl(),
|
|
||||||
Settings::generalSettings().qrModel(),
|
|
||||||
stream));
|
|
||||||
} else {
|
|
||||||
config.url
|
|
||||||
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
|
|
||||||
config.providerRequest
|
|
||||||
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
|
||||||
}
|
|
||||||
|
|
||||||
LLMCore::ContextData context = prepareContext(editor, range, instructions);
|
|
||||||
|
|
||||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
|
||||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
|
||||||
provider->prepareRequest(
|
|
||||||
config.providerRequest,
|
|
||||||
promptTemplate,
|
|
||||||
context,
|
|
||||||
LLMCore::RequestType::QuickRefactoring,
|
|
||||||
enableTools,
|
|
||||||
enableThinking);
|
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
|
||||||
m_lastRequestId = requestId;
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
|
||||||
this,
|
|
||||||
&QuickRefactorHandler::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::requestFailed,
|
|
||||||
this,
|
|
||||||
&QuickRefactorHandler::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|
||||||
TextEditor::TextEditorWidget *editor,
|
|
||||||
const Utils::Text::Range &range,
|
|
||||||
const QString &instructions)
|
|
||||||
{
|
|
||||||
LLMCore::ContextData context;
|
|
||||||
|
|
||||||
auto textDocument = editor->textDocument();
|
|
||||||
Context::DocumentReaderQtCreator documentReader;
|
|
||||||
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
|
|
||||||
|
|
||||||
if (!documentInfo.document) {
|
|
||||||
LOG_MESSAGE("Error: Document is not available");
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
|
||||||
int cursorPos = cursor.position();
|
|
||||||
|
|
||||||
Context::DocumentContextReader
|
|
||||||
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
|
||||||
|
|
||||||
QString taggedContent;
|
|
||||||
bool readFullFile = Settings::quickRefactorSettings().readFullFile();
|
|
||||||
|
|
||||||
if (cursor.hasSelection()) {
|
|
||||||
int selStart = cursor.selectionStart();
|
|
||||||
int selEnd = cursor.selectionEnd();
|
|
||||||
|
|
||||||
QTextBlock startBlock = documentInfo.document->findBlock(selStart);
|
|
||||||
int startLine = startBlock.blockNumber();
|
|
||||||
int startColumn = selStart - startBlock.position();
|
|
||||||
|
|
||||||
QTextBlock endBlock = documentInfo.document->findBlock(selEnd);
|
|
||||||
int endLine = endBlock.blockNumber();
|
|
||||||
int endColumn = selEnd - endBlock.position();
|
|
||||||
|
|
||||||
QString contextBefore;
|
|
||||||
if (readFullFile) {
|
|
||||||
contextBefore = reader.readWholeFileBefore(startLine, startColumn);
|
|
||||||
} else {
|
|
||||||
contextBefore = reader.getContextBefore(
|
|
||||||
startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString selectedText = cursor.selectedText();
|
|
||||||
selectedText.replace(QChar(0x2029), "\n");
|
|
||||||
|
|
||||||
QString contextAfter;
|
|
||||||
if (readFullFile) {
|
|
||||||
contextAfter = reader.readWholeFileAfter(endLine, endColumn);
|
|
||||||
} else {
|
|
||||||
contextAfter = reader.getContextAfter(
|
|
||||||
endLine, endColumn, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
taggedContent = contextBefore;
|
|
||||||
if (selStart == cursorPos) {
|
|
||||||
taggedContent += "<cursor><selection_start>" + selectedText + "<selection_end>";
|
|
||||||
} else {
|
|
||||||
taggedContent += "<selection_start>" + selectedText + "<selection_end><cursor>";
|
|
||||||
}
|
|
||||||
taggedContent += contextAfter;
|
|
||||||
} else {
|
|
||||||
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
|
||||||
int line = block.blockNumber();
|
|
||||||
int column = cursorPos - block.position();
|
|
||||||
|
|
||||||
QString contextBefore;
|
|
||||||
if (readFullFile) {
|
|
||||||
contextBefore = reader.readWholeFileBefore(line, column);
|
|
||||||
} else {
|
|
||||||
contextBefore = reader.getContextBefore(
|
|
||||||
line, column, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString contextAfter;
|
|
||||||
if (readFullFile) {
|
|
||||||
contextAfter = reader.readWholeFileAfter(line, column);
|
|
||||||
} else {
|
|
||||||
contextAfter = reader.getContextAfter(
|
|
||||||
line, column, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
taggedContent = contextBefore + "<cursor>" + contextAfter;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (project) {
|
|
||||||
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
|
|
||||||
project, LLMCore::RulesContext::QuickRefactor);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
systemPrompt += "\n\nFile information:";
|
|
||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
|
||||||
|
|
||||||
systemPrompt += "\n\nCode context with position markers:";
|
|
||||||
systemPrompt += taggedContent;
|
|
||||||
|
|
||||||
systemPrompt += "\n\nOutput format:";
|
|
||||||
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
|
|
||||||
"between<selection_start><selection_end> or be "
|
|
||||||
"inserted at cursor position<cursor>";
|
|
||||||
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
|
|
||||||
"code block markers";
|
|
||||||
systemPrompt += "\n- The output should be ready to insert directly into the editor";
|
|
||||||
systemPrompt += "\n- Follow the existing code style and indentation patterns";
|
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
|
||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
|
||||||
}
|
|
||||||
|
|
||||||
context.systemPrompt = systemPrompt;
|
|
||||||
|
|
||||||
QVector<LLMCore::Message> messages;
|
|
||||||
messages.append(
|
|
||||||
{"user",
|
|
||||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
|
||||||
: instructions});
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleLLMResponse(
|
|
||||||
const QString &response, const QJsonObject &request, bool isComplete)
|
|
||||||
{
|
|
||||||
if (request["id"].toString() != m_lastRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
|
|
||||||
QString cleanedResponse = response.trimmed();
|
|
||||||
if (cleanedResponse.startsWith("```")) {
|
|
||||||
int firstNewLine = cleanedResponse.indexOf('\n');
|
|
||||||
int lastFence = cleanedResponse.lastIndexOf("```");
|
|
||||||
|
|
||||||
if (firstNewLine != -1 && lastFence > firstNewLine) {
|
|
||||||
cleanedResponse
|
|
||||||
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
|
|
||||||
} else if (lastFence != -1) {
|
|
||||||
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RefactorResult result;
|
|
||||||
result.newText = cleanedResponse;
|
|
||||||
result.insertRange = m_currentRange;
|
|
||||||
result.success = true;
|
|
||||||
result.editor = m_currentEditor;
|
|
||||||
|
|
||||||
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
|
||||||
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
|
||||||
LOG_MESSAGE(cleanedResponse);
|
|
||||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
|
||||||
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::cancelRequest()
|
|
||||||
{
|
|
||||||
if (m_isRefactoringInProgress) {
|
|
||||||
auto id = m_lastRequestId;
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.key() == id) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
ctx.provider->cancelRequest(id);
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = "Refactoring request was cancelled";
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
if (requestId == m_lastRequestId) {
|
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
handleLLMResponse(fullText, request, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
if (requestId == m_lastRequestId) {
|
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
result.editor = m_currentEditor;
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <QJsonObject>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
#include <utils/textutils.h>
|
|
||||||
|
|
||||||
#include <context/ContextManager.hpp>
|
|
||||||
#include <context/IDocumentReader.hpp>
|
|
||||||
#include <llmcore/ContextData.hpp>
|
|
||||||
#include <llmcore/Provider.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
struct RefactorResult
|
|
||||||
{
|
|
||||||
QString newText;
|
|
||||||
Utils::Text::Range insertRange;
|
|
||||||
bool success;
|
|
||||||
QString errorMessage;
|
|
||||||
TextEditor::TextEditorWidget *editor{nullptr};
|
|
||||||
};
|
|
||||||
|
|
||||||
class QuickRefactorHandler : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
|
||||||
~QuickRefactorHandler() override;
|
|
||||||
|
|
||||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
|
||||||
|
|
||||||
void cancelRequest();
|
|
||||||
bool isProcessing() const { return m_isRefactoringInProgress; }
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void prepareAndSendRequest(
|
|
||||||
TextEditor::TextEditorWidget *editor,
|
|
||||||
const QString &instructions,
|
|
||||||
const Utils::Text::Range &range);
|
|
||||||
|
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
|
||||||
LLMCore::ContextData prepareContext(
|
|
||||||
TextEditor::TextEditorWidget *editor,
|
|
||||||
const Utils::Text::Range &range,
|
|
||||||
const QString &instructions);
|
|
||||||
|
|
||||||
struct RequestContext
|
|
||||||
{
|
|
||||||
QJsonObject originalRequest;
|
|
||||||
LLMCore::Provider *provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
|
||||||
TextEditor::TextEditorWidget *m_currentEditor;
|
|
||||||
Utils::Text::Range m_currentRange;
|
|
||||||
bool m_isRefactoringInProgress;
|
|
||||||
QString m_lastRequestId;
|
|
||||||
Context::ContextManager m_contextManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
511
README.md
@@ -2,9 +2,10 @@
|
|||||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
[](https://discord.gg/BGMkUsXUgf)
|
[](https://discord.gg/BGMkUsXUgf)
|
||||||
|
|
||||||
 QodeAssist is a comprehensive AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion, interactive chat with multiple interface options, inline quick refactoring, and AI function calling capabilities for C++ and QML development. Supporting both local providers (Ollama, llama.cpp, LM Studio) and cloud services (Claude, OpenAI, Google AI, Mistral AI), QodeAssist enhances your productivity with context-aware AI assistance, project-specific rules, and extensive customization options directly in your Qt development environment.
|
 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.
|
||||||
|
|
||||||
⚠️ **Important Notice About Paid Providers**
|
⚠️ **Important Notice About Paid Providers**
|
||||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||||
@@ -12,43 +13,56 @@
|
|||||||
> - The QodeAssist developer bears no responsibility for any charges incurred
|
> - The QodeAssist developer bears no responsibility for any charges incurred
|
||||||
> - Please carefully review the provider's pricing and your account settings before use
|
> - Please carefully review the provider's pricing and your account settings before use
|
||||||
|
|
||||||
|
⚠️ **Commercial Support and Custom Development**
|
||||||
|
> The QodeAssist developer offers commercial services for:
|
||||||
|
> - Adapting the plugin for specific Qt Creator versions
|
||||||
|
> - Custom development for particular operating systems
|
||||||
|
> - Integration with specific language models
|
||||||
|
> - Implementing custom features and modifications
|
||||||
|
>
|
||||||
|
> For commercial inquiries, please contact: qodeassist.dev@pm.me
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
1. [Overview](#overview)
|
1. [Overview](#overview)
|
||||||
2. [Install Plugin](#install-plugin-to-qtcreator)
|
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
|
||||||
3. [Configuration](#configuration)
|
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
||||||
4. [Features](#features)
|
4. [Configure for OpenAI](#configure-for-openai)
|
||||||
5. [Context Layers](#context-layers)
|
5. [Configure for using Ollama](#configure-for-using-ollama)
|
||||||
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
6. [System Prompt Configuration](#system-prompt-configuration)
|
||||||
7. [Hotkeys](#hotkeys)
|
7. [File Context Features](#file-context-features)
|
||||||
8. [Troubleshooting](#troubleshooting)
|
8. [Template-Model Compatibility](#template-model-compatibility)
|
||||||
9. [Development Progress](#development-progress)
|
9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||||
10. [Support the Development](#support-the-development-of-qodeassist)
|
10. [Development Progress](#development-progress)
|
||||||
11. [How to Build](#how-to-build)
|
11. [Hotkeys](#hotkeys)
|
||||||
|
12. [Troubleshooting](#troubleshooting)
|
||||||
|
13. [Support the Development](#support-the-development-of-qodeassist)
|
||||||
|
14. [How to Build](#how-to-build)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
- AI-powered code completion
|
||||||
|
- Chat functionality:
|
||||||
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
|
- Side and Bottom panels
|
||||||
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
|
- Chat history autosave and restore
|
||||||
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library
|
- Token usage monitoring and management
|
||||||
- **File Context**: Attach or link files for better AI understanding
|
- Attach files for one-time code analysis
|
||||||
- **Tool Calling**: AI can read project files, search code, and access diagnostics
|
- Link files for persistent context with auto update in conversations
|
||||||
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
|
- Automatic syncing with open editor files (optional)
|
||||||
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates
|
- Support for multiple LLM providers:
|
||||||
|
- Ollama
|
||||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
- OpenAI
|
||||||
|
- Anthropic Claude
|
||||||
|
- LM Studio
|
||||||
|
- OpenAI-compatible providers(eg. https://openrouter.ai)
|
||||||
|
- Extensive library of model-specific templates
|
||||||
|
- Custom template support
|
||||||
|
- Easy configuration and model selection
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Code completion: (click to expand)</summary>
|
<summary>Code completion: (click to expand)</summary>
|
||||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Quick refactor in code: (click to expand)</summary>
|
|
||||||
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Multiline Code completion: (click to expand)</summary>
|
<summary>Multiline Code completion: (click to expand)</summary>
|
||||||
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
||||||
@@ -64,58 +78,15 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||||
</details>
|
</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>
|
<details>
|
||||||
<summary>Automatic syncing with open editor files: (click to expand)</summary>
|
<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">
|
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example how tools works: (click to expand)</summary>
|
|
||||||
<img width="600" alt="ToolsDemo" src="https://github.com/user-attachments/assets/cf6273ad-d5c8-47fc-81e6-23d929547f6c">
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Install plugin to QtCreator
|
## Install plugin to QtCreator
|
||||||
|
|
||||||
### Method 1: Using QodeAssistUpdater (Beta)
|
|
||||||
|
|
||||||
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Automatic Qt Creator version detection
|
|
||||||
- Install, update, or remove plugin with single command
|
|
||||||
- List all available plugin versions
|
|
||||||
- Install specific plugin version
|
|
||||||
- Checksum verification
|
|
||||||
- Non-interactive mode for CI/CD
|
|
||||||
|
|
||||||
**Installation:**
|
|
||||||
|
|
||||||
Download pre-built binary from [QodeAssistUpdater releases](https://github.com/Palm1r/QodeAssistUpdater/releases) or build from source
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check current status and available updates
|
|
||||||
./qodeassist-updater --status
|
|
||||||
|
|
||||||
# Install latest version
|
|
||||||
./qodeassist-updater --install
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
|
||||||
|
|
||||||
### Method 2: Manual Installation
|
|
||||||
|
|
||||||
1. Install Latest Qt Creator
|
1. Install Latest Qt Creator
|
||||||
2. Download the QodeAssist plugin for your Qt Creator
|
2. Download the QodeAssist plugin for your Qt Creator
|
||||||
- Remove old version plugin if already was installed
|
- Remove old version plugin if already was installed
|
||||||
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
|
|
||||||
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
|
|
||||||
3. Launch Qt Creator and install the plugin:
|
3. Launch Qt Creator and install the plugin:
|
||||||
- Go to:
|
- Go to:
|
||||||
- MacOS: Qt Creator -> About Plugins...
|
- MacOS: Qt Creator -> About Plugins...
|
||||||
@@ -123,242 +94,169 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
|
|||||||
- Click on "Install Plugin..."
|
- Click on "Install Plugin..."
|
||||||
- Select the downloaded QodeAssist plugin archive file
|
- Select the downloaded QodeAssist plugin archive file
|
||||||
|
|
||||||
## Configuration
|
## Configure for Anthropic Claude
|
||||||
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
|
2. Go to Provider Settings tab and configure Claude api key
|
||||||
|
3. Return to General tab and configure:
|
||||||
### Supported Providers
|
- Set "Claude" as the provider for code completion or/and chat assistant
|
||||||
|
- Set the Claude URL (https://api.anthropic.com)
|
||||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
|
||||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
- Choose the Claude template for code completion or/and chat
|
||||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
|
||||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
|
|
||||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
|
||||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
|
||||||
- **LM Studio** - Local LLM provider
|
|
||||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
|
||||||
|
|
||||||
### Additional Configuration
|
|
||||||
|
|
||||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
|
||||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Code Completion
|
|
||||||
- AI-powered intelligent code completion
|
|
||||||
- Support for C++ and QML
|
|
||||||
- Context-aware suggestions
|
|
||||||
- Multiline completions
|
|
||||||
|
|
||||||
### Chat Assistant
|
|
||||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
|
||||||
- Chat history with auto-save and restore
|
|
||||||
- Token usage monitoring
|
|
||||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
|
||||||
- Automatic syncing with open editor files (optional)
|
|
||||||
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
|
||||||
|
|
||||||
### Quick Refactoring
|
|
||||||
- Inline code refactoring directly in the editor with AI assistance
|
|
||||||
- Selection-based improvements with instant code replacement
|
|
||||||
- Built-in quick actions (repeat, improve, alternative)
|
|
||||||
- **Custom instructions library** with search and autocomplete
|
|
||||||
- Create, edit, and manage reusable refactoring templates
|
|
||||||
- Combine base instructions with specific details
|
|
||||||
- **[Learn more](docs/quick-refactoring.md)**
|
|
||||||
|
|
||||||
### Tools & Function Calling
|
|
||||||
- Read project files
|
|
||||||
- List and search in project
|
|
||||||
- Access linter/compiler issues
|
|
||||||
- Enabled by default (can be disabled)
|
|
||||||
|
|
||||||
## Context Layers
|
|
||||||
|
|
||||||
QodeAssist uses a flexible prompt composition system that adapts to different contexts. Here's how prompts are constructed for each feature:
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Code Completion (FIM Models)</strong> - Codestral, Qwen2.5-Coder, DeepSeek-Coder (click to expand)</summary>
|
<summary>Example of Claude settings: (click to expand)</summary>
|
||||||
|
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ CODE COMPLETION (FIM Models) │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
|
|
||||||
│ │
|
|
||||||
│ 1. System Prompt (from Code Completion Settings - FIM variant) │
|
|
||||||
│ 2. Project Rules: │
|
|
||||||
│ └─ .qodeassist/rules/completion/*.md │
|
|
||||||
│ 3. Open Files Context (optional, if enabled): │
|
|
||||||
│ └─ Currently open editor files │
|
|
||||||
│ 4. Code Context: │
|
|
||||||
│ ├─ Code before cursor (prefix) │
|
|
||||||
│ └─ Code after cursor (suffix) │
|
|
||||||
│ │
|
|
||||||
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles + │
|
|
||||||
│ CodeBefore, │
|
|
||||||
│ Suffix: CodeAfter) │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Configure for OpenAI
|
||||||
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
|
2. Go to Provider Settings tab and configure OpenAI api key
|
||||||
|
3. Return to General tab and configure:
|
||||||
|
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
||||||
|
- Set the OpenAI URL (https://api.openai.com)
|
||||||
|
- Select your preferred model (e.g., gpt-4o)
|
||||||
|
- Choose the OpenAI template for code completion or/and chat
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Code Completion (Non-FIM Models)</strong> - DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct (click to expand)</summary>
|
<summary>Example of OpenAI settings: (click to expand)</summary>
|
||||||
|
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ CODE COMPLETION (Non-FIM/Chat Models) │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
|
|
||||||
│ │
|
|
||||||
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant) │
|
|
||||||
│ └─ Includes response formatting instructions │
|
|
||||||
│ 2. Project Rules: │
|
|
||||||
│ └─ .qodeassist/rules/completion/*.md │
|
|
||||||
│ 3. Open Files Context (optional, if enabled): │
|
|
||||||
│ └─ Currently open editor files │
|
|
||||||
│ 4. Code Context: │
|
|
||||||
│ ├─ File information (language, path) │
|
|
||||||
│ ├─ Code before cursor │
|
|
||||||
│ ├─ <cursor> marker │
|
|
||||||
│ └─ Code after cursor │
|
|
||||||
│ 5. User Message: "Complete the code at cursor position" │
|
|
||||||
│ │
|
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
|
||||||
│ [User: OpenFiles + Context + CompletionRequest] │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Configure for using Ollama
|
||||||
|
|
||||||
|
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||||
|
2. Install a language models in Ollama via terminal. For example, you can run:
|
||||||
|
|
||||||
|
For standard computers (minimum 8GB RAM):
|
||||||
|
```
|
||||||
|
ollama run qwen2.5-coder:7b
|
||||||
|
```
|
||||||
|
For better performance (16GB+ RAM):
|
||||||
|
```
|
||||||
|
ollama run qwen2.5-coder:14b
|
||||||
|
```
|
||||||
|
For high-end systems (32GB+ RAM):
|
||||||
|
```
|
||||||
|
ollama run qwen2.5-coder:32b
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
|
||||||
|
2. Navigate to the "Qode Assist" tab
|
||||||
|
3. On the "General" page, verify:
|
||||||
|
- Ollama is selected as your LLM provider
|
||||||
|
- The URL is set to http://localhost:11434
|
||||||
|
- Your installed model appears in the model selection
|
||||||
|
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
|
||||||
|
4. Click Apply if you made any changes
|
||||||
|
|
||||||
|
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Chat Assistant</strong> - Interactive coding assistant (click to expand)</summary>
|
<summary>Example of Ollama settings: (click to expand)</summary>
|
||||||
|
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ CHAT ASSISTANT │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
|
||||||
│ 2. Project Rules: │
|
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
|
||||||
│ └─ .qodeassist/rules/chat/*.md │
|
|
||||||
│ 3. File Context (optional): │
|
|
||||||
│ ├─ Attached files (manual) │
|
|
||||||
│ ├─ Linked files (persistent) │
|
|
||||||
│ └─ Open editor files (if auto-sync enabled) │
|
|
||||||
│ 4. Tool Definitions (if enabled): │
|
|
||||||
│ ├─ ReadProjectFileByName │
|
|
||||||
│ ├─ ListProjectFiles │
|
|
||||||
│ ├─ SearchInProject │
|
|
||||||
│ └─ GetIssuesList │
|
|
||||||
│ 5. Conversation History │
|
|
||||||
│ 6. User Message │
|
|
||||||
│ │
|
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
|
|
||||||
│ [History: Previous messages] │
|
|
||||||
│ [User: FileContext + UserMessage] │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
## System Prompt Configuration
|
||||||
<summary><strong>Quick Refactoring</strong> - Inline code improvements (click to expand)</summary>
|
|
||||||
|
|
||||||
```
|
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.
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ QUICK REFACTORING │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ 1. System Prompt (from Quick Refactor Settings) │
|
|
||||||
│ 2. Project Rules: │
|
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
|
||||||
│ └─ .qodeassist/rules/quickrefactor/*.md │
|
|
||||||
│ 3. Code Context: │
|
|
||||||
│ ├─ File information (language, path) │
|
|
||||||
│ ├─ Code before selection (configurable amount) │
|
|
||||||
│ ├─ <selection_start> marker │
|
|
||||||
│ ├─ Selected code (or current line) │
|
|
||||||
│ ├─ <selection_end> marker │
|
|
||||||
│ ├─ <cursor> marker (position within selection) │
|
|
||||||
│ └─ Code after selection (configurable amount) │
|
|
||||||
│ 4. Refactor Instruction: │
|
|
||||||
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
|
|
||||||
│ ├─ Custom Instruction (from library) │
|
|
||||||
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
|
|
||||||
│ │ quick_refactor/instructions/*.json │
|
|
||||||
│ └─ Additional Details (optional user input) │
|
|
||||||
│ 5. Tool Definitions (if enabled) │
|
|
||||||
│ │
|
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
|
||||||
│ [User: Context + Markers + Instruction + Details] │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
## File Context Features
|
||||||
|
|
||||||
### Key Points
|
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.
|
||||||
|
|
||||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
### Attached Files
|
||||||
- **System Prompts** are configured independently for each feature in Settings
|
|
||||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
|
||||||
- FIM models: Direct completion prompt
|
|
||||||
- Non-FIM models: Prompt includes response formatting instructions
|
|
||||||
- **Quick Refactor** has its own provider/model configuration, independent from Chat
|
|
||||||
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
|
||||||
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
|
||||||
|
|
||||||
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
Attachments are designed for one-time code analysis and specific queries:
|
||||||
|
- Files are included only in the current message
|
||||||
|
- Content is discarded after the message is processed
|
||||||
|
- Ideal for:
|
||||||
|
- Getting specific feedback on code changes
|
||||||
|
- Code review requests
|
||||||
|
- Analyzing isolated code segments
|
||||||
|
- Quick implementation questions
|
||||||
|
- Files can be attached using the paperclip icon in the chat interface
|
||||||
|
- Multiple files can be attached to a single message
|
||||||
|
|
||||||
|
### Linked Files
|
||||||
|
|
||||||
|
Linked files provide persistent context throughout the conversation:
|
||||||
|
|
||||||
|
- Files remain accessible for the entire chat session
|
||||||
|
- Content is included in every message exchange
|
||||||
|
- Files are automatically refreshed - always using latest content from disk
|
||||||
|
- Perfect for:
|
||||||
|
- Long-term refactoring discussions
|
||||||
|
- Complex architectural changes
|
||||||
|
- Multi-file implementations
|
||||||
|
- Maintaining context across related questions
|
||||||
|
- Can be managed using the link icon in the chat interface
|
||||||
|
- Supports automatic syncing with open editor files (can be enabled in settings)
|
||||||
|
- Files can be added/removed at any time during the conversation
|
||||||
|
|
||||||
|
## Template-Model Compatibility
|
||||||
|
|
||||||
|
| Template | Compatible Models | Purpose |
|
||||||
|
|----------|------------------|----------|
|
||||||
|
| CodeLlama FIM | `codellama:code` | Code completion |
|
||||||
|
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
|
||||||
|
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion |
|
||||||
|
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion |
|
||||||
|
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
|
||||||
|
| Alpaca | `starcoder2:instruct` | Chat assistance |
|
||||||
|
| Basic Chat| `Messages without tokens` | Chat assistance |
|
||||||
|
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance |
|
||||||
|
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
|
||||||
|
| Llama3 | `llama3 model family` | Chat assistance |
|
||||||
|
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance |
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
| Qt Creator Version | QodeAssist Version |
|
- QtCreator 15.0.1 - 0.4.8 - 0.4.x
|
||||||
|-------------------|-------------------|
|
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
|
||||||
| 17.0.0+ | 0.6.0 - 0.x.x |
|
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
||||||
| 16.0.2 | 0.5.13 - 0.x.x |
|
- QtCreator 14.0.1 - 0.2.2 plugin version and below
|
||||||
| 16.0.1 | 0.5.7 - 0.5.13 |
|
|
||||||
| 16.0.0 | 0.5.2 - 0.5.6 |
|
|
||||||
| 15.0.1 | 0.4.8 - 0.5.1 |
|
|
||||||
| 15.0.0 | 0.4.0 - 0.4.7 |
|
|
||||||
| 14.0.2 | 0.2.3 - 0.3.x |
|
|
||||||
| 14.0.1 | ≤ 0.2.2 |
|
|
||||||
|
|
||||||
## Hotkeys
|
|
||||||
|
|
||||||
All hotkeys can be customized in Qt Creator Settings. Default hotkeys:
|
|
||||||
|
|
||||||
| Action | macOS | Windows/Linux |
|
|
||||||
|--------|-------|--------------|
|
|
||||||
| Open chat window | ⌥⌘W | Ctrl+Alt+W |
|
|
||||||
| Close chat window | ⌥⌘S | Ctrl+Alt+S |
|
|
||||||
| Manual code suggestion | ⌥⌘Q | Ctrl+Alt+Q |
|
|
||||||
| Accept full suggestion | Tab | Tab |
|
|
||||||
| Accept word | ⌥→ | Alt+→ |
|
|
||||||
| Quick refactor | ⌥⌘R | Ctrl+Alt+R |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Having issues with QodeAssist? Check our [detailed troubleshooting guide](docs/troubleshooting.md) for:
|
|
||||||
|
|
||||||
- Connection issues and provider URLs
|
|
||||||
- Model and template compatibility
|
|
||||||
- Platform-specific issues (Linux, macOS, Windows)
|
|
||||||
- Resetting settings to defaults
|
|
||||||
- Common problems and solutions
|
|
||||||
|
|
||||||
For additional support, join our [Discord Community](https://discord.gg/BGMkUsXUgf) or check [GitHub Issues](https://github.com/Palm1r/QodeAssist/issues).
|
|
||||||
|
|
||||||
## Development Progress
|
## Development Progress
|
||||||
|
|
||||||
- [x] Code completion functionality
|
- [x] Basic plugin with code autocomplete functionality
|
||||||
- [x] Chat assistant with multiple panels
|
- [x] Improve and automate settings
|
||||||
- [x] Diff sharing with models
|
- [x] Add chat functionality
|
||||||
- [x] Tools/function calling support
|
- [x] Sharing diff with model
|
||||||
- [x] Project-specific rules
|
- [ ] Sharing project source with model
|
||||||
- [ ] Full project source sharing
|
- [ ] Support for more providers and models
|
||||||
- [ ] Additional provider support
|
|
||||||
- [ ] MCP (Model Context Protocol) support
|
## Hotkeys
|
||||||
|
|
||||||
|
- To call manual request to suggestion, you can use or change it in settings
|
||||||
|
- on Mac: Option + Command + Q
|
||||||
|
- on Windows: Ctrl + Alt + Q
|
||||||
|
- To insert the full suggestion, you can use the TAB key
|
||||||
|
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If QodeAssist is having problems connecting to the LLM provider, please check the following:
|
||||||
|
|
||||||
|
1. Verify the IP address and port:
|
||||||
|
|
||||||
|
- For Ollama, the default is usually http://localhost:11434
|
||||||
|
- For LM Studio, the default is usually http://localhost:1234
|
||||||
|
|
||||||
|
2. Check the endpoint:
|
||||||
|
|
||||||
|
Make sure the endpoint in the settings matches the one required by your provider
|
||||||
|
- For Ollama, it should be /api/generate
|
||||||
|
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
|
||||||
|
|
||||||
|
3. Confirm that the selected model and template are compatible:
|
||||||
|
|
||||||
|
Ensure you've chosen the correct model in the "Select Models" option
|
||||||
|
Verify that the selected prompt template matches the model you're using
|
||||||
|
|
||||||
|
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
|
||||||
|
1. Open Qt Creator settings
|
||||||
|
2. Navigate to the "Qode Assist" tab
|
||||||
|
3. Pick settings page for reset
|
||||||
|
4. Click on the "Reset Page to Defaults" button
|
||||||
|
- The API key will not reset
|
||||||
|
- Select model after reset
|
||||||
|
|
||||||
## Support the development of QodeAssist
|
## Support the development of QodeAssist
|
||||||
If you find QodeAssist helpful, there are several ways you can support the project:
|
If you find QodeAssist helpful, there are several ways you can support the project:
|
||||||
@@ -379,43 +277,20 @@ Every contribution, no matter how small, is greatly appreciated and helps keep t
|
|||||||
|
|
||||||
## How to Build
|
## How to Build
|
||||||
|
|
||||||
### Prerequisites
|
Create a build directory and run
|
||||||
- CMake 3.16+
|
|
||||||
- C++20 compatible compiler
|
|
||||||
- Qt Creator development files
|
|
||||||
|
|
||||||
### Build Steps
|
cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
|
||||||
|
cmake --build .
|
||||||
|
|
||||||
1. Create a build directory:
|
where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator build directory, or to a
|
||||||
|
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
|
||||||
```bash
|
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
|
||||||
mkdir build && cd build
|
relative or absolute path to this plugin directory.
|
||||||
```
|
|
||||||
|
|
||||||
2. Configure and build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
|
|
||||||
cmake --build .
|
|
||||||
```
|
|
||||||
|
|
||||||
**Path specifications:**
|
|
||||||
- `<path_to_qtcreator>`:
|
|
||||||
- **Windows/Linux**: Qt Creator build directory or combined binary package
|
|
||||||
- **macOS**: `Qt Creator.app/Contents/Resources/`
|
|
||||||
- `<path_to_plugin_source>`: Path to this plugin directory
|
|
||||||
|
|
||||||
## For Contributors
|
## For Contributors
|
||||||
|
|
||||||
### Code Style
|
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
|
||||||
|
C++ code style: check use .clang-fortmat in project
|
||||||
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
|
||||||
- **C++**: Use `.clang-format` configuration in the project root
|
|
||||||
- Run formatting before submitting PRs
|
|
||||||
|
|
||||||
### Development Guidelines
|
|
||||||
|
|
||||||
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "RefactorSuggestion.hpp"
|
|
||||||
#include "LLMSuggestion.hpp"
|
|
||||||
|
|
||||||
#include <QTextBlock>
|
|
||||||
#include <QTextCursor>
|
|
||||||
#include <QTextDocument>
|
|
||||||
|
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
#include <logger/Logger.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
QString extractLeadingWhitespace(const QString &text)
|
|
||||||
{
|
|
||||||
QString indent;
|
|
||||||
int firstLineEnd = text.indexOf('\n');
|
|
||||||
QString firstLine = (firstLineEnd != -1) ? text.left(firstLineEnd) : text;
|
|
||||||
for (int i = 0; i < firstLine.length(); ++i) {
|
|
||||||
if (firstLine[i].isSpace()) {
|
|
||||||
indent += firstLine[i];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return indent;
|
|
||||||
}
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
RefactorSuggestion::RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument)
|
|
||||||
: TextEditor::TextSuggestion([&suggestion, sourceDocument]() {
|
|
||||||
Data expandedData = suggestion;
|
|
||||||
|
|
||||||
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
|
|
||||||
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
|
|
||||||
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
|
||||||
endPos = qBound(0, endPos, sourceDocument->characterCount());
|
|
||||||
|
|
||||||
if (startPos != endPos) {
|
|
||||||
QTextCursor startCursor(sourceDocument);
|
|
||||||
startCursor.setPosition(startPos);
|
|
||||||
int startPosInBlock = startCursor.positionInBlock();
|
|
||||||
|
|
||||||
if (startPosInBlock > 0) {
|
|
||||||
startCursor.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor endCursor(sourceDocument);
|
|
||||||
endCursor.setPosition(endPos);
|
|
||||||
int endPosInBlock = endCursor.positionInBlock();
|
|
||||||
|
|
||||||
if (endPosInBlock > 0) {
|
|
||||||
endCursor.movePosition(QTextCursor::EndOfBlock);
|
|
||||||
if (!endCursor.atEnd()) {
|
|
||||||
endCursor.movePosition(QTextCursor::NextCharacter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
|
|
||||||
sourceDocument, startCursor.position());
|
|
||||||
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
|
|
||||||
sourceDocument, endCursor.position());
|
|
||||||
|
|
||||||
expandedData.range = Utils::Text::Range(expandedBegin, expandedEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
return expandedData;
|
|
||||||
}(), sourceDocument)
|
|
||||||
, m_suggestionData(suggestion)
|
|
||||||
{
|
|
||||||
const QString refactoredText = suggestion.text;
|
|
||||||
|
|
||||||
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
|
|
||||||
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
|
|
||||||
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
|
||||||
endPos = qBound(0, endPos, sourceDocument->characterCount());
|
|
||||||
|
|
||||||
QTextCursor startCursor(sourceDocument);
|
|
||||||
startCursor.setPosition(startPos);
|
|
||||||
|
|
||||||
if (startPos == endPos) {
|
|
||||||
QTextBlock block = startCursor.block();
|
|
||||||
QString blockText = block.text();
|
|
||||||
int startPosInBlock = startCursor.positionInBlock();
|
|
||||||
|
|
||||||
QString leftText = blockText.left(startPosInBlock);
|
|
||||||
QString rightText = blockText.mid(startPosInBlock);
|
|
||||||
|
|
||||||
QString displayText = leftText + refactoredText + rightText;
|
|
||||||
replacementDocument()->setPlainText(displayText);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
QTextCursor fullLinesCursor(sourceDocument);
|
|
||||||
fullLinesCursor.setPosition(startPos);
|
|
||||||
fullLinesCursor.movePosition(QTextCursor::StartOfBlock);
|
|
||||||
int fullLinesStart = fullLinesCursor.position();
|
|
||||||
|
|
||||||
fullLinesCursor.setPosition(endPos);
|
|
||||||
fullLinesCursor.movePosition(QTextCursor::EndOfBlock);
|
|
||||||
int fullLinesEnd = fullLinesCursor.position();
|
|
||||||
|
|
||||||
fullLinesCursor.setPosition(fullLinesStart);
|
|
||||||
fullLinesCursor.setPosition(fullLinesEnd, QTextCursor::KeepAnchor);
|
|
||||||
QString fullLinesText = fullLinesCursor.selectedText();
|
|
||||||
fullLinesText.replace(QChar(0x2029), "\n");
|
|
||||||
|
|
||||||
QString oldIndent = extractLeadingWhitespace(fullLinesText);
|
|
||||||
QString newIndent = extractLeadingWhitespace(refactoredText);
|
|
||||||
|
|
||||||
QString displayText = refactoredText;
|
|
||||||
if (newIndent.length() < oldIndent.length()) {
|
|
||||||
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
|
|
||||||
QStringList lines = refactoredText.split('\n');
|
|
||||||
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
|
|
||||||
lines[0] = indentDiff + lines[0];
|
|
||||||
displayText = lines.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
replacementDocument()->setPlainText(displayText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RefactorSuggestion::apply()
|
|
||||||
{
|
|
||||||
const QString text = m_suggestionData.text;
|
|
||||||
const Utils::Text::Range range = m_suggestionData.range;
|
|
||||||
|
|
||||||
const QTextCursor startCursor = range.begin.toTextCursor(sourceDocument());
|
|
||||||
const QTextCursor endCursor = range.end.toTextCursor(sourceDocument());
|
|
||||||
|
|
||||||
const int startPos = startCursor.position();
|
|
||||||
const int endPos = endCursor.position();
|
|
||||||
|
|
||||||
QTextCursor editCursor(sourceDocument());
|
|
||||||
editCursor.beginEditBlock();
|
|
||||||
|
|
||||||
if (startPos == endPos) {
|
|
||||||
editCursor.setPosition(startPos);
|
|
||||||
editCursor.insertText(text);
|
|
||||||
} else {
|
|
||||||
editCursor.setPosition(startPos);
|
|
||||||
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
|
||||||
QString selectedText = editCursor.selectedText();
|
|
||||||
selectedText.replace(QChar(0x2029), "\n");
|
|
||||||
|
|
||||||
QString oldIndent = extractLeadingWhitespace(selectedText);
|
|
||||||
QString newIndent = extractLeadingWhitespace(text);
|
|
||||||
|
|
||||||
QString textToInsert = text;
|
|
||||||
if (newIndent.length() < oldIndent.length()) {
|
|
||||||
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
|
|
||||||
QStringList lines = text.split('\n');
|
|
||||||
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
|
|
||||||
lines[0] = indentDiff + lines[0];
|
|
||||||
textToInsert = lines.join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editCursor.setPosition(startPos);
|
|
||||||
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
|
||||||
editCursor.removeSelectedText();
|
|
||||||
editCursor.insertText(textToInsert);
|
|
||||||
}
|
|
||||||
|
|
||||||
editCursor.endEditBlock();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RefactorSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
|
||||||
{
|
|
||||||
Q_UNUSED(widget)
|
|
||||||
return apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RefactorSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
|
||||||
{
|
|
||||||
Q_UNUSED(widget)
|
|
||||||
return apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <texteditor/texteditor.h>
|
|
||||||
#include <texteditor/textsuggestion.h>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
class RefactorSuggestion : public TextEditor::TextSuggestion
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument);
|
|
||||||
|
|
||||||
bool apply() override;
|
|
||||||
|
|
||||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
|
||||||
|
|
||||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Data m_suggestionData;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 "RefactorSuggestionHoverHandler.hpp"
|
|
||||||
#include "RefactorSuggestion.hpp"
|
|
||||||
|
|
||||||
#include <QColor>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScopeGuard>
|
|
||||||
#include <QTextBlock>
|
|
||||||
#include <QTextCursor>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include <texteditor/textdocumentlayout.h>
|
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
#include <utils/theme/theme.h>
|
|
||||||
#include <utils/tooltip/tooltip.h>
|
|
||||||
|
|
||||||
#include <logger/Logger.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
RefactorSuggestionHoverHandler::RefactorSuggestionHoverHandler()
|
|
||||||
{
|
|
||||||
setPriority(Priority_Suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RefactorSuggestionHoverHandler::setSuggestionRange(const Utils::Text::Range &range)
|
|
||||||
{
|
|
||||||
m_suggestionRange = range;
|
|
||||||
m_hasSuggestion = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RefactorSuggestionHoverHandler::clearSuggestionRange()
|
|
||||||
{
|
|
||||||
m_hasSuggestion = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RefactorSuggestionHoverHandler::identifyMatch(
|
|
||||||
TextEditor::TextEditorWidget *editorWidget,
|
|
||||||
int pos,
|
|
||||||
ReportPriority report)
|
|
||||||
{
|
|
||||||
|
|
||||||
QScopeGuard cleanup([&] { report(Priority_None); });
|
|
||||||
|
|
||||||
if (!editorWidget->suggestionVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor cursor(editorWidget->document());
|
|
||||||
cursor.setPosition(pos);
|
|
||||||
m_block = cursor.block();
|
|
||||||
|
|
||||||
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
|
|
||||||
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
|
|
||||||
TextEditor::TextBlockUserData::suggestion(m_block));
|
|
||||||
#else
|
|
||||||
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
|
|
||||||
if (!userData) {
|
|
||||||
LOG_MESSAGE("RefactorSuggestionHoverHandler: No user data in block");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!suggestion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup.dismiss();
|
|
||||||
report(Priority_Suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RefactorSuggestionHoverHandler::operateTooltip(
|
|
||||||
TextEditor::TextEditorWidget *editorWidget,
|
|
||||||
const QPoint &point)
|
|
||||||
{
|
|
||||||
Q_UNUSED(point)
|
|
||||||
|
|
||||||
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
|
|
||||||
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
|
|
||||||
TextEditor::TextBlockUserData::suggestion(m_block));
|
|
||||||
#else
|
|
||||||
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
|
|
||||||
if (!userData) {
|
|
||||||
LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: No user data in block");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!suggestion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *widget = new QWidget();
|
|
||||||
auto *layout = new QHBoxLayout(widget);
|
|
||||||
layout->setContentsMargins(4, 3, 4, 3);
|
|
||||||
layout->setSpacing(6);
|
|
||||||
|
|
||||||
const QColor normalBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
|
|
||||||
const QColor hoverBg = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
|
|
||||||
const QColor selectedBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
|
|
||||||
const QColor textColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
|
||||||
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
|
|
||||||
const QColor successColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
|
|
||||||
const QColor errorColor = Utils::creatorColor(Utils::Theme::TextColorError);
|
|
||||||
|
|
||||||
auto *applyButton = new QPushButton("✓ Apply", widget);
|
|
||||||
applyButton->setFocusPolicy(Qt::NoFocus);
|
|
||||||
applyButton->setToolTip("Apply refactoring (Tab)");
|
|
||||||
applyButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
applyButton->setStyleSheet(QString(
|
|
||||||
"QPushButton {"
|
|
||||||
" background-color: %1;"
|
|
||||||
" color: %2;"
|
|
||||||
" border: 1px solid %3;"
|
|
||||||
" border-radius: 3px;"
|
|
||||||
" padding: 4px 12px;"
|
|
||||||
" font-weight: bold;"
|
|
||||||
" font-size: 11px;"
|
|
||||||
" min-width: 60px;"
|
|
||||||
"}"
|
|
||||||
"QPushButton:hover {"
|
|
||||||
" background-color: %4;"
|
|
||||||
" border-color: %2;"
|
|
||||||
"}"
|
|
||||||
"QPushButton:pressed {"
|
|
||||||
" background-color: %5;"
|
|
||||||
"}")
|
|
||||||
.arg(selectedBg.name())
|
|
||||||
.arg(successColor.name())
|
|
||||||
.arg(borderColor.name())
|
|
||||||
.arg(selectedBg.lighter(110).name())
|
|
||||||
.arg(selectedBg.darker(110).name()));
|
|
||||||
QObject::connect(applyButton, &QPushButton::clicked, widget, [this]() {
|
|
||||||
Utils::ToolTip::hide();
|
|
||||||
if (m_applyCallback) {
|
|
||||||
m_applyCallback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
auto *dismissButton = new QPushButton("✕ Dismiss", widget);
|
|
||||||
dismissButton->setFocusPolicy(Qt::NoFocus);
|
|
||||||
dismissButton->setToolTip("Dismiss refactoring (Esc)");
|
|
||||||
dismissButton->setCursor(Qt::PointingHandCursor);
|
|
||||||
dismissButton->setStyleSheet(QString(
|
|
||||||
"QPushButton {"
|
|
||||||
" background-color: %1;"
|
|
||||||
" color: %2;"
|
|
||||||
" border: 1px solid %3;"
|
|
||||||
" border-radius: 3px;"
|
|
||||||
" padding: 4px 12px;"
|
|
||||||
" font-size: 11px;"
|
|
||||||
" min-width: 60px;"
|
|
||||||
"}"
|
|
||||||
"QPushButton:hover {"
|
|
||||||
" background-color: %4;"
|
|
||||||
" color: %5;"
|
|
||||||
" border-color: %5;"
|
|
||||||
"}"
|
|
||||||
"QPushButton:pressed {"
|
|
||||||
" background-color: %6;"
|
|
||||||
"}")
|
|
||||||
.arg(normalBg.name())
|
|
||||||
.arg(textColor.name())
|
|
||||||
.arg(borderColor.name())
|
|
||||||
.arg(hoverBg.name())
|
|
||||||
.arg(errorColor.name())
|
|
||||||
.arg(hoverBg.darker(110).name()));
|
|
||||||
QObject::connect(dismissButton, &QPushButton::clicked, widget, [this]() {
|
|
||||||
Utils::ToolTip::hide();
|
|
||||||
if (m_dismissCallback) {
|
|
||||||
m_dismissCallback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
layout->addWidget(applyButton);
|
|
||||||
layout->addWidget(dismissButton);
|
|
||||||
|
|
||||||
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
|
|
||||||
QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft())
|
|
||||||
- Utils::ToolTip::offsetFromPosition();
|
|
||||||
pos.ry() -= widget->sizeHint().height();
|
|
||||||
|
|
||||||
Utils::ToolTip::show(pos, widget, editorWidget);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 <QTextBlock>
|
|
||||||
|
|
||||||
#include <texteditor/basehoverhandler.h>
|
|
||||||
#include <utils/textutils.h>
|
|
||||||
|
|
||||||
namespace TextEditor {
|
|
||||||
class TextEditorWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Hover handler for refactoring suggestions
|
|
||||||
*
|
|
||||||
* Shows interactive tooltip with Apply/Dismiss buttons when hovering over
|
|
||||||
* a refactoring suggestion in the editor.
|
|
||||||
*/
|
|
||||||
class RefactorSuggestionHoverHandler : public TextEditor::BaseHoverHandler
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
using ApplyCallback = std::function<void()>;
|
|
||||||
using DismissCallback = std::function<void()>;
|
|
||||||
|
|
||||||
RefactorSuggestionHoverHandler();
|
|
||||||
|
|
||||||
void setSuggestionRange(const Utils::Text::Range &range);
|
|
||||||
void clearSuggestionRange();
|
|
||||||
bool hasSuggestion() const { return m_hasSuggestion; }
|
|
||||||
|
|
||||||
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
|
|
||||||
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void identifyMatch(TextEditor::TextEditorWidget *editorWidget,
|
|
||||||
int pos,
|
|
||||||
ReportPriority report) override;
|
|
||||||
|
|
||||||
void operateTooltip(TextEditor::TextEditorWidget *editorWidget,
|
|
||||||
const QPoint &point) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Utils::Text::Range m_suggestionRange;
|
|
||||||
bool m_hasSuggestion = false;
|
|
||||||
ApplyCallback m_applyCallback;
|
|
||||||
DismissCallback m_dismissCallback;
|
|
||||||
QTextBlock m_block;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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}
|
|
||||||
)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
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
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||