Compare commits
493 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e580b8792 | |||
| 5352fd4f0b | |||
| 6e9db1552c | |||
| c302138568 | |||
| 75cbc46808 | |||
| 1070de6e6e | |||
| b3432cd76f | |||
| f99e4aefb0 | |||
| 191de10926 | |||
| 8492ef29b2 | |||
| fea9ecddc8 | |||
| a26d475806 | |||
| 1cd19aa5d1 | |||
| 4956d6ab7d | |||
| 2d92b8fa53 | |||
| 161d77ac04 | |||
| 89797639cf | |||
| 4d79760481 | |||
| 3be70556ec | |||
| a1ff17eef0 | |||
| 6c1d9ddc0e | |||
| 6ff6901421 | |||
| a2f3ae4f64 | |||
| 6937b48fbf | |||
| c6e77c59d3 | |||
| 655471aec6 | |||
| af90d3cad2 | |||
| 9b90aaa06e | |||
| e7110810f8 | |||
| 1848d44503 | |||
| db82fb08e8 | |||
| 9117572f82 | |||
| a143cc8e20 | |||
| eae2b748d5 | |||
| 64bca47290 | |||
| 531fce96b5 | |||
| e7e437590a | |||
| 00b7287e08 | |||
| 5a49a2e7eb | |||
| 3b56c1f07a | |||
| d483ca372d | |||
| dfd9979450 | |||
| 6cb0b14b18 | |||
| 43b64b9166 | |||
| 608103b92e | |||
| e1025df21e | |||
| fad2453dbe | |||
| 5e1530715c | |||
| dfac209c23 | |||
| cab8718979 | |||
| 5dc28fc1ad | |||
| 1122332423 | |||
| 7e878cdbf8 | |||
| c95b20d6d4 | |||
| 70c610997a | |||
| 8a4bf54fff | |||
| db7da29fa4 | |||
| a0a76f2665 | |||
| 56354e8d87 | |||
| b7322be00c | |||
| 254fac246d | |||
| 0365018834 | |||
| 755be518be | |||
| fe82b48bef | |||
| 8a338ecb69 | |||
| 238ca00227 | |||
| 18fb2b530f | |||
| f0d2e42680 | |||
| ff0f994ec6 | |||
| 45df27e749 | |||
| 02863003a9 | |||
| 002b8e01e5 | |||
| f54d1185aa | |||
| bcb0c6f761 | |||
| d285ab6117 | |||
| 5ae6f9e3bf | |||
| fb5903e44f | |||
| ce66c8e4f7 | |||
| 5f094887e7 | |||
| 69d9af1a97 | |||
| 86b52bf858 | |||
| cac6068ee7 | |||
| 8d495dd1bf | |||
| 906c161729 | |||
| ebd71daf3d | |||
| 84770abb20 | |||
| b4e8bdf6da | |||
| d2b28093a6 | |||
| bde58fb9aa | |||
| d4b6f8976b | |||
| cd08b5d919 | |||
| f6de03f601 | |||
| 1a08eebe92 | |||
| ea4f8b9df9 | |||
| 7f77f7175d | |||
| bed42f9098 | |||
| 10b924d78a | |||
| 8aa37c5c8c | |||
| f8b87da2ca | |||
| 7663bd34af | |||
| a52c86c6f0 | |||
| ac53296e85 | |||
| c688cba3dd | |||
| ff750c271a | |||
| d0f8c1098f | |||
| 5cde6ffac7 | |||
| 8c6f1e514b | |||
| 99cd79aac8 | |||
| d2b6c11569 | |||
| ec1b5bdf5f | |||
| 561661b476 | |||
| 76309be0a6 | |||
| 5969d530bd | |||
| 809f1c6614 | |||
| 851e681cf5 | |||
| f2f3b7cce0 | |||
| 5b7a9b681c | |||
| 29af277139 | |||
| a45786bd00 | |||
| 695b35b510 | |||
| 5a23ab9c5a | |||
| c36dffea93 | |||
| 4b7eed2779 | |||
| 88c11c4702 | |||
| 543c79161d | |||
| aa2edf5954 | |||
| 894fec860a | |||
| e4324f8e80 | |||
| 6a0198ae9b | |||
| e136d6056a | |||
| ff027b12af | |||
| 0bdf77f38d | |||
| 21814e8809 | |||
| d732e2f9aa | |||
| bf6d09a068 | |||
| c3f2011c29 | |||
| af3fdb58ff | |||
| 637a4d9d4c | |||
| 7e2345773f | |||
| 14a5ddbdd8 | |||
| e178b7daa7 | |||
| 4b353d5091 | |||
| f7ba7b95be | |||
| 6ae95fec45 | |||
| dad8ab2bf3 | |||
| 25a6983de0 | |||
| 4e05abc7d2 | |||
| 784529e344 | |||
| 155153a763 | |||
| 9225c0c1a9 | |||
| 43adc95857 | |||
| ee672f2cda | |||
| a3edb8a577 | |||
| 407d3b11c0 | |||
| 285e739074 | |||
| f7e748ba7e | |||
| acb1306321 | |||
| 8b38ecc29b | |||
| cfb364f033 | |||
| 2fe6850a06 | |||
| 3e9506ca92 | |||
| d24adff0f5 | |||
| 447324eb07 | |||
| 4ca494cc51 | |||
| 8a80dbe8f5 | |||
| 2b539bbdeb | |||
| 3f2c146df1 | |||
| 9a54f04a0d | |||
| 7a33425d1a | |||
| 711aa672f2 | |||
| 8cb6a2f6d2 | |||
| 2f9622e23e | |||
| 674b1fecde | |||
| b36d01d2c7 | |||
| 615175bea8 | |||
| 7515599acb | |||
| 3652d4d5d9 | |||
| 75677770b2 | |||
| 329a1efd5d | |||
| 27760a3b99 | |||
| a93b3cd7f5 | |||
| bacde51d71 | |||
| 418578743a | |||
| 56e5ef22f1 | |||
| e90933d713 | |||
| 5b9c67c2d8 | |||
| fe84a2a303 | |||
| 62de53c306 | |||
| 7c6a10936c | |||
| 032c9bbbf3 | |||
| 8906f98038 | |||
| 5126092449 | |||
| 9d2d70fc63 | |||
| ffaf6bd61b | |||
| 79218d8412 | |||
| 7e6e526ac8 | |||
| 80646e2af0 | |||
| 5808a892c1 | |||
| d58ff90458 | |||
| 7d06ab04dc | |||
| 9d40e8ca25 | |||
| 5b16c5403a | |||
| 4ddbe0b8b9 | |||
| f41e063c02 | |||
| 9d7d084448 | |||
| 1ca1ffc629 | |||
| 8419577ae5 | |||
| 91a6a88130 | |||
| be38abc505 | |||
| f2e0afb6b8 | |||
| 3cf07238fd | |||
| b98f85a997 | |||
| 085659483f | |||
| 8a1fd5438e | |||
| 78f69e82a5 | |||
| 3d770f91c7 | |||
| c724bace06 | |||
| 719065ebfc | |||
| a218064a4f | |||
| 13cd12b00a | |||
| ed59be4199 | |||
| 7dd8b3d085 | |||
| 3839d6896c | |||
| 6b86637dcb | |||
| 58c3e26e7f | |||
| 98e1047bf1 | |||
| b6f36d61ae | |||
| f2f453ccc8 | |||
| 1bcfd749d5 | |||
| e66f467214 | |||
| c9a3cdaf25 | |||
| 7c483f89cd | |||
| 6c323642e4 | |||
| 3a494d5254 | |||
| 44b3b0cc0c | |||
| 3aae923d43 | |||
| f94c79a5ff | |||
| 9a5047618d | |||
| 90beebf2ee | |||
| 521261e0a3 | |||
| 5536de146c | |||
| 81ac3c71fb | |||
| 61ca5c9a1b | |||
| 8a167bf248 | |||
| ab97f39ea4 | |||
| 0d3493e7f6 | |||
| 1d062e1fe4 | |||
| 5dceb5cd19 | |||
| 69a8aa80d9 | |||
| e218699e64 | |||
| 3dc0d910bf | |||
| f9f2a86cea | |||
| 247256d4a4 | |||
| bcf7b6c226 | |||
| 29a3939c64 | |||
| cb3464170e | |||
| ca0fb5efbb | |||
| d8a01504a3 | |||
| 3b188740e8 | |||
| 0d22e1866e | |||
| 61196cae90 | |||
| 102bb114a1 | |||
| e507d7ee17 | |||
| ed55c829af | |||
| d651a246de | |||
| c8e0f3268e | |||
| 84025ec843 | |||
| f6fd411b2d | |||
| 903eb50e7a | |||
| e8f7f031b6 | |||
| 90ae6cd1c0 | |||
| 2ad0117498 | |||
| 9d58565de3 | |||
| 8dba9b4baa | |||
| 7ba615a72d | |||
| 1b06a651f0 | |||
| 912c3d8c04 | |||
| e924029ec2 | |||
| d96f44d42c | |||
| bd25736a55 | |||
| 60936f6d84 | |||
| 7d23d0323f | |||
| 1fa6a225a4 | |||
| 31133e3378 | |||
| a2f15fc843 | |||
| 2a0beb6c4c | |||
| e836b86569 | |||
| 288fefebe5 | |||
| 528badbf1e | |||
| b789e42602 | |||
| 4bf955462f | |||
| 5b99e68e53 | |||
| 0f1b277ef7 | |||
| 56995c9edf | |||
| 45aba6b6be | |||
| 1dfb3feb96 | |||
| 2c49d45297 | |||
| 31145f191b | |||
| 9096adde6f | |||
| b8e578d2d7 | |||
| 4e45774bce | |||
| 928490d31f | |||
| 97163cf6c9 | |||
| f85c162692 | |||
| 258053d826 | |||
| bf63ae5714 | |||
| ae76850e78 | |||
| bf3c0b3aa0 | |||
| 9add61c805 | |||
| add86d2e67 | |||
| a6c909d34d | |||
| 2814dec3e5 | |||
| 1b86b60de8 | |||
| 4b7f638731 | |||
| de046f0529 | |||
| e975e143b1 | |||
| c97c0f62e8 | |||
| 61fded34ea | |||
| 289a19ac1a | |||
| 43ac662671 | |||
| 1d64d2afc9 | |||
| 9db61119aa | |||
| 70481b3116 | |||
| 511f5b36eb | |||
| 35012865c7 | |||
| f27429aa66 | |||
| 113d5adcf4 | |||
| 30ea89cdc2 | |||
| 13469edce6 | |||
| ee2c3950e8 | |||
| d04e5bc967 | |||
| d8ef9d0120 | |||
| e544e46d76 | |||
| 63f0900511 | |||
| 7dee6f62c0 | |||
| dc06ea2ed5 | |||
| fc5e1adc0d | |||
| 93e59fb2dc | |||
| cd2a56cde0 | |||
| 09cde8fd3d | |||
| ac8080542d | |||
| 7376a11a05 | |||
| 10e8b16caf | |||
| a38debb140 | |||
| 844ac35a59 | |||
| 16b77a5722 | |||
| c070fd5cfd | |||
| 882047d7b2 | |||
| b692402897 | |||
| 8102ba95f9 | |||
| f8bb9998ab | |||
| 6dab055ca2 | |||
| 7b31fff9f2 | |||
| be9156fd0e | |||
| 657413344d | |||
| 5f3deb44b9 | |||
| 55e2b24b8d | |||
| 76c17f03dd | |||
| 19c25043fb | |||
| 56b5ea8e68 | |||
| b475f15e3d | |||
| 31f4516e7b | |||
| bfdbc755e3 | |||
| 30964d90d5 | |||
| 1261f913bb | |||
| 36d5242a1f | |||
| 6503887091 | |||
| 50087aa744 | |||
| 4f2dc0c450 | |||
| 80fe388bdd | |||
| 8375d85f7d | |||
| 54b2cc7011 | |||
| f209cb75a2 | |||
| 5e813ba402 | |||
| 7af8fc2ddc | |||
| 46300f7635 | |||
| 0a1c941d8b | |||
| f86182408d | |||
| 252db4c5f7 | |||
| e5af3a2884 | |||
| bb543d1f40 | |||
| a184916d7b | |||
| 7ad8ddfee4 | |||
| 0ed6fb4a6b | |||
| 00fce5db99 | |||
| 251a9bae03 | |||
| 3dba9d7abe | |||
| 45b0f3f18e | |||
| 4432d4019d | |||
| f679d76d43 | |||
| 29f94561ef | |||
| cd6c766ed2 | |||
| 6d3bc362b3 | |||
| 87393b681f | |||
| 5d496fee58 | |||
| 9902623ba0 | |||
| 61f1f0ae4f | |||
| bc93bce03b | |||
| 85d039cbd5 | |||
| 2acaef553d | |||
| b141e54e3e | |||
| 1ec6098210 | |||
| 9c945f066b | |||
| 4a82e9c046 | |||
| 838d69623c | |||
| 693e429bdd | |||
| 496d8feb66 | |||
| 40a568ebd9 | |||
| 5b43eb4fd2 | |||
| 9c2516cd4c | |||
| 2257e6e45f | |||
| 80eda8c167 | |||
| 3db2691114 | |||
| bf518b4a01 | |||
| 46829720d8 | |||
| 9158a3ac0d | |||
| d6e02d9d2a | |||
| 9c8cac4e3a | |||
| 965af4a945 | |||
| 95f29fefc7 | |||
| 1dd50b6c83 | |||
| 146e772514 | |||
| 4b851f1662 | |||
| 6fea300825 | |||
| 14bf0e6c94 | |||
| 0c045e65df | |||
| 15138b4644 | |||
| 5c98de7440 | |||
| b808d0ec10 | |||
| 30fcd7e019 | |||
| 7442256bab | |||
| 8be279a5fd | |||
| d77e13cddb | |||
| 162c068431 | |||
| 4e8ff55355 | |||
| 8df21e96bd | |||
| 57bec94ee4 | |||
| 1760a2d5ff | |||
| 1649a246e1 | |||
| 0ab4b51520 | |||
| d235d0fcdf | |||
| 1cbde3d55b | |||
| 9903ac8f7b | |||
| fbe363689f | |||
| d7f0cc92e6 | |||
| 8311db5b08 | |||
| cd6dd94cd2 | |||
| b559a17407 | |||
| 9745997952 | |||
| aac58d6933 | |||
| 93c69df1b9 | |||
| 4b0b589d0e | |||
| 04c44f5916 | |||
| 6e56646b4c | |||
| c89fe1451b | |||
| 938636ab48 | |||
| e3a2b5a64c | |||
| 0ae12e0fc6 | |||
| 793b855819 | |||
| 8e052ff45c | |||
| 2fb876ff00 | |||
| cd1a9e16e0 | |||
| f07610df5c | |||
| 7cd35082b2 | |||
| c1dd59e65c | |||
| faaf59f163 | |||
| 539a220771 | |||
| 397dd33a96 | |||
| 9361c27d6e | |||
| 15af137728 | |||
| 24ad5fd015 | |||
| 216c28aa5e | |||
| f64ea42071 | |||
| 314ba06db1 | |||
| 384e07ba62 | |||
| 4d06541a36 | |||
| 356f28a97b | |||
| b5ca11ed38 | |||
| d49cd07dd0 | |||
| 8e61651bae | |||
| 1e0063a7bb | |||
| 44add994b9 | |||
| 99136d7b76 | |||
| da7abbd9be | |||
| 16035d6de6 | |||
| 4891cc4b1e | |||
| 7370258485 | |||
| a1df602182 | |||
| a1b813206a | |||
| ada65e9ef7 | |||
| 753365ea52 | |||
| 4d9adf75ff | |||
| a974b0aa82 |
108
.clang-format
Normal file
@ -0,0 +1,108 @@
|
||||
# .clang-format from Qt Creator
|
||||
# https://github.com/qt-creator/qt-creator/blob/master/.clang-format
|
||||
#
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/clang-format.json
|
||||
#
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveAssignments: None
|
||||
AlignConsecutiveDeclarations: None
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: true
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakBeforeBinaryOperators: All
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- forever # avoids { wrapped to next line
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeCategories:
|
||||
- Regex: '^<Q.*'
|
||||
Priority: 200
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
# Do not add QT_BEGIN_NAMESPACE/QT_END_NAMESPACE as this will indent lines in between.
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 88
|
||||
PenaltyBreakBeforeFirstCallParameter: 300
|
||||
PenaltyBreakComment: 500
|
||||
PenaltyBreakFirstLessLess: 400
|
||||
PenaltyBreakString: 600
|
||||
PenaltyExcessCharacter: 50
|
||||
PenaltyReturnTypeOnItsOwnLine: 300
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: CaseSensitive
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: true
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++17
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
15
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: qodeassist
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee 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']
|
||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Log**
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
109
.github/scripts/plugin.json
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "QodeAssist",
|
||||
"vendor": "Petr Mironychev",
|
||||
"tags": [
|
||||
"code assistant",
|
||||
"llm",
|
||||
"ai"
|
||||
],
|
||||
"compatibility": "Qt 6.8.3",
|
||||
"platforms": [
|
||||
"Windows",
|
||||
"macOS",
|
||||
"Linux"
|
||||
],
|
||||
"license": "GPLv3",
|
||||
"version": "0.5.11",
|
||||
"status": "draft",
|
||||
"is_pack": false,
|
||||
"released_at": null,
|
||||
"version_history": [
|
||||
{
|
||||
"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,
|
||||
"released_at": "2025-05-01T17:00:00Z"
|
||||
}
|
||||
],
|
||||
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
||||
"small_icon": "https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41",
|
||||
"description_paragraphs": [
|
||||
{
|
||||
"header": "Description",
|
||||
"text": [
|
||||
"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."
|
||||
]
|
||||
}
|
||||
],
|
||||
"description_links": [
|
||||
{
|
||||
"url": "https://github.com/Palm1r/QodeAssist",
|
||||
"link_text": "Site"
|
||||
}
|
||||
],
|
||||
"description_images": [
|
||||
{
|
||||
"url": "https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a",
|
||||
"image_label": "Code Completion"
|
||||
}
|
||||
],
|
||||
"copyright": "(C) Petr Mironychev",
|
||||
"download_history": {
|
||||
"download_count": 0
|
||||
},
|
||||
"plugin_sets": []
|
||||
}
|
||||
147
.github/scripts/registerPlugin.js
vendored
Normal file
@ -0,0 +1,147 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const updatePluginData = (plugin, env, pluginQtcData) => {
|
||||
const dictionary_platform = {
|
||||
'Windows': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Windows-x64.7z`,
|
||||
'Linux': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Linux-x64.7z`,
|
||||
'macOS': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-macOS-universal.7z`
|
||||
};
|
||||
|
||||
plugin.core_compat_version = env.QT_CREATOR_VERSION_INTERNAL;
|
||||
plugin.core_version = env.QT_CREATOR_VERSION_INTERNAL;
|
||||
plugin.status = "draft";
|
||||
|
||||
plugin.plugins.forEach(pluginsEntry => {
|
||||
pluginsEntry.url = dictionary_platform[plugin.host_os];
|
||||
pluginsEntry.meta_data = pluginQtcData;
|
||||
});
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const createNewPluginData = (env, platform, pluginQtcData) => {
|
||||
const pluginJson = {
|
||||
"status": "draft",
|
||||
"core_compat_version": "<placeholder>",
|
||||
"core_version": "<placeholder>",
|
||||
"host_os": platform,
|
||||
"host_os_version": "0", // TODO: pass the real data
|
||||
"host_os_architecture": "x86_64", // TODO: pass the real data
|
||||
"plugins": [
|
||||
{
|
||||
"url": "",
|
||||
"size": 5000, // TODO: check if it is needed, pass the real data
|
||||
"meta_data": {},
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
updatePluginData(pluginJson, env, pluginQtcData);
|
||||
return pluginJson;
|
||||
}
|
||||
|
||||
const updateServerPluginJson = (endJsonData, pluginQtcData, env) => {
|
||||
// Update the global data in mainData
|
||||
endJsonData.name = pluginQtcData.Name;
|
||||
endJsonData.vendor = pluginQtcData.Vendor;
|
||||
endJsonData.version = pluginQtcData.Version;
|
||||
endJsonData.copyright = pluginQtcData.Copyright;
|
||||
endJsonData.status = "draft";
|
||||
|
||||
endJsonData.version_history[0].version = pluginQtcData.Version;
|
||||
|
||||
endJsonData.description_paragraphs = [
|
||||
{
|
||||
header: "Description",
|
||||
text: [
|
||||
pluginQtcData.Description
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let found = false;
|
||||
// Update or Add the plugin data for the current Qt Creator version
|
||||
for (const plugin of endJsonData.plugin_sets) {
|
||||
if (plugin.core_compat_version === env.QT_CREATOR_VERSION_INTERNAL) {
|
||||
updatePluginData(plugin, env, pluginQtcData);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
for (const platform of ['Windows', 'Linux', 'macOS']) {
|
||||
endJsonData.plugin_sets.push(createNewPluginData(env, platform, pluginQtcData));
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated JSON file
|
||||
const serverPluginJsonPath = path.join(__dirname, `${env.PLUGIN_NAME}.json`);
|
||||
fs.writeFileSync(serverPluginJsonPath, JSON.stringify(endJsonData, null, 2), 'utf8');
|
||||
};
|
||||
|
||||
const request = async (type, url, token, data) => {
|
||||
const response = await fetch(url, {
|
||||
method: type,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: data ? JSON.stringify(data) : undefined
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorResponse = await response.json();
|
||||
console.error(`${type} Request Error Response:`, errorResponse); // Log the error response
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
const put = (url, token, data) => request('PUT', url, token, data)
|
||||
const post = (url, token, data) => request('POST', url, token, data)
|
||||
const get = (url, token) => request('GET', url, token)
|
||||
|
||||
const purgeCache = async (env) => {
|
||||
try {
|
||||
await post(`${env.API_URL}api/v1/cache/purgeall`, env.TOKEN, {});
|
||||
console.log('Cache purged successfully');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const env = {
|
||||
PLUGIN_DOWNLOAD_URL: process.env.PLUGIN_DOWNLOAD_URL || process.argv[2],
|
||||
PLUGIN_NAME: process.env.PLUGIN_NAME || process.argv[3],
|
||||
QT_CREATOR_VERSION: process.env.QT_CREATOR_VERSION || process.argv[4],
|
||||
QT_CREATOR_VERSION_INTERNAL: process.env.QT_CREATOR_VERSION_INTERNAL || process.argv[5],
|
||||
TOKEN: process.env.TOKEN || process.argv[6],
|
||||
API_URL: process.env.API_URL || process.argv[7] || ''
|
||||
};
|
||||
|
||||
const pluginQtcData = require(`../../${env.PLUGIN_NAME}-origin/${env.PLUGIN_NAME}.json`);
|
||||
const templateFileData = require('./plugin.json');
|
||||
|
||||
if (env.API_URL === '') {
|
||||
updateServerPluginJson(templateFileData, pluginQtcData, env);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const response = await get(`${env.API_URL}api/v1/admin/extensions?search=${env.PLUGIN_NAME}`, env.TOKEN);
|
||||
if (response.items.length > 0 && response.items[0].extension_id !== '') {
|
||||
const pluginId = response.items[0].extension_id;
|
||||
console.log('Plugin found. Updating the plugin');
|
||||
updateServerPluginJson(response.items[0], pluginQtcData, env);
|
||||
|
||||
await put(`${env.API_URL}api/v1/admin/extensions/${pluginId}`, env.TOKEN, response.items[0]);
|
||||
} else {
|
||||
console.log('No plugin found. Creating a new plugin');
|
||||
updateServerPluginJson(templateFileData, pluginQtcData, env);
|
||||
await post(`${env.API_URL}api/v1/admin/extensions`, env.TOKEN, templateFileData);
|
||||
}
|
||||
// await purgeCache(env);
|
||||
}
|
||||
|
||||
main().then(() => console.log('JSON file updated successfully'));
|
||||
199
.github/workflows/build_cmake.yml
vendored
@ -1,19 +1,24 @@
|
||||
name: Build plugin
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: QodeAssist
|
||||
QT_VERSION: 6.7.2
|
||||
QT_CREATOR_VERSION: 14.0.0
|
||||
QT_CREATOR_SNAPSHOT: NO
|
||||
MACOS_DEPLOYMENT_TARGET: "11.0"
|
||||
CMAKE_VERSION: "3.29.6"
|
||||
NINJA_VERSION: "1.12.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.config.name }}
|
||||
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
outputs:
|
||||
tag: ${{ steps.git.outputs.tag }}
|
||||
@ -23,76 +28,61 @@ jobs:
|
||||
- {
|
||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||
os: windows-latest,
|
||||
platform: windows_x64,
|
||||
cc: "cl", cxx: "cl",
|
||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||
}
|
||||
- {
|
||||
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
|
||||
os: ubuntu-latest,
|
||||
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
|
||||
os: ubuntu-22.04,
|
||||
platform: linux_x64,
|
||||
cc: "gcc", cxx: "g++"
|
||||
}
|
||||
- {
|
||||
name: "macOS Latest Clang", artifact: "macOS-universal",
|
||||
os: macos-latest,
|
||||
platform: mac_x64,
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
if (${{github.ref}} MATCHES "tags/v(.*)")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\n")
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
|
||||
else()
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
|
||||
execute_process(
|
||||
COMMAND git rev-parse --short HEAD
|
||||
OUTPUT_VARIABLE short_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
|
||||
endif()
|
||||
|
||||
- name: Download Ninja and CMake
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(cmake_version "$ENV{CMAKE_VERSION}")
|
||||
set(ninja_version "$ENV{NINJA_VERSION}")
|
||||
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
|
||||
with:
|
||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(ninja_suffix "win.zip")
|
||||
set(cmake_suffix "windows-x86_64.zip")
|
||||
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(ninja_suffix "linux.zip")
|
||||
set(cmake_suffix "linux-x86_64.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
|
||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||
set(ninja_suffix "mac.zip")
|
||||
set(cmake_suffix "macos-universal.tar.gz")
|
||||
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
|
||||
endif()
|
||||
|
||||
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
|
||||
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
|
||||
|
||||
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
|
||||
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
|
||||
|
||||
# Add to PATH environment variable
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
|
||||
set(path_separator ":")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(path_separator ";")
|
||||
endif()
|
||||
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
|
||||
|
||||
if (NOT "${{ runner.os }}" STREQUAL "Windows")
|
||||
execute_process(
|
||||
COMMAND chmod +x ninja
|
||||
COMMAND chmod +x ${cmake_dir}/cmake
|
||||
)
|
||||
endif()
|
||||
|
||||
- name: Install system libs
|
||||
- name: Install dependencies
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||
@ -100,7 +90,13 @@ jobs:
|
||||
COMMAND sudo apt update
|
||||
)
|
||||
execute_process(
|
||||
COMMAND sudo apt install libgl1-mesa-dev
|
||||
COMMAND sudo apt install
|
||||
# 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
|
||||
)
|
||||
if (NOT result EQUAL 0)
|
||||
@ -112,14 +108,19 @@ jobs:
|
||||
id: qt
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
set(qt_version "$ENV{QT_VERSION}")
|
||||
set(qt_version "${{ matrix.qt_config.qt_version }}")
|
||||
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(url_os "windows_x86")
|
||||
set(qt_package_arch_suffix "win64_msvc2019_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2019_64")
|
||||
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
|
||||
set(qt_package_arch_suffix "win64_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_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")
|
||||
set(url_os "linux_x64")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -128,15 +129,23 @@ jobs:
|
||||
set(qt_package_arch_suffix "linux_gcc_64")
|
||||
endif()
|
||||
set(qt_dir_prefix "${qt_version}/gcc_64")
|
||||
set(qt_package_suffix "-Linux-RHEL_8_8-GCC-Linux-RHEL_8_8-X86_64")
|
||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
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")
|
||||
set(url_os "mac_x64")
|
||||
set(qt_package_arch_suffix "clang_64")
|
||||
set(qt_dir_prefix "${qt_version}/macos")
|
||||
set(qt_package_suffix "-MacOS-MacOS_13-Clang-MacOS-MacOS_13-X86_64-ARM64")
|
||||
if (qt_version VERSION_LESS "6.9.1")
|
||||
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
|
||||
else()
|
||||
set(qt_package_suffix "-MacOS-MacOS_15-Clang-MacOS-MacOS_15-X86_64-ARM64")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}")
|
||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
||||
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
|
||||
|
||||
file(READ ./Updates.xml updates_xml)
|
||||
@ -146,7 +155,7 @@ jobs:
|
||||
file(MAKE_DIRECTORY qt6)
|
||||
|
||||
# Save the path for other steps
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6/${qt_dir_prefix}" qt_dir)
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" qt_dir)
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}")
|
||||
|
||||
message("Downloading Qt to ${qt_dir}")
|
||||
@ -156,7 +165,7 @@ jobs:
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||
endfunction()
|
||||
|
||||
foreach(package qtbase qtdeclarative)
|
||||
foreach(package qtbase qtdeclarative qttools)
|
||||
downloadAndExtract(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
${package}.7z
|
||||
@ -165,11 +174,17 @@ jobs:
|
||||
|
||||
foreach(package qt5compat qtshadertools)
|
||||
downloadAndExtract(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
${package}.7z
|
||||
)
|
||||
endforeach()
|
||||
|
||||
function(downloadAndExtractLibicu url archive)
|
||||
message("Downloading ${url}")
|
||||
file(DOWNLOAD "${url}" ./${archive} SHOW_PROGRESS)
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../../${archive} WORKING_DIRECTORY qt6/lib)
|
||||
endfunction()
|
||||
|
||||
# uic depends on libicu*.so
|
||||
if ("${{ runner.os }}" STREQUAL "Linux")
|
||||
if (qt_version VERSION_LESS "6.7.0")
|
||||
@ -177,47 +192,26 @@ jobs:
|
||||
else()
|
||||
set(uic_suffix "Rhel8.6-x86_64")
|
||||
endif()
|
||||
downloadAndExtract(
|
||||
downloadAndExtractLibicu(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
|
||||
icu.7z
|
||||
)
|
||||
endif()
|
||||
|
||||
- name: Download Qt Creator
|
||||
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
|
||||
with:
|
||||
version: ${{ matrix.qt_config.qt_creator_version }}
|
||||
unzip-to: 'qtcreator'
|
||||
platform: ${{ matrix.config.platform }}
|
||||
|
||||
- name: Extract Qt Creator
|
||||
id: qt_creator
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "$ENV{QT_CREATOR_VERSION}")
|
||||
|
||||
set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source")
|
||||
set(qtc_snapshot "$ENV{QT_CREATOR_SNAPSHOT}")
|
||||
if (qtc_snapshot)
|
||||
set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source/${qtc_snapshot}")
|
||||
endif()
|
||||
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(qtc_platform "windows_x64")
|
||||
elseif ("${{ runner.os }}" STREQUAL "Linux")
|
||||
set(qtc_platform "linux_x64")
|
||||
elseif ("${{ runner.os }}" STREQUAL "macOS")
|
||||
set(qtc_platform "mac_x64")
|
||||
endif()
|
||||
|
||||
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir)
|
||||
# Save the path for other steps
|
||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}")
|
||||
|
||||
file(MAKE_DIRECTORY qtcreator)
|
||||
|
||||
message("Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}")
|
||||
|
||||
foreach(package qtcreator qtcreator_dev)
|
||||
file(DOWNLOAD
|
||||
"${qtc_base_url}/${qtc_platform}/${package}.7z" ./${package}.7z SHOW_PROGRESS)
|
||||
execute_process(COMMAND
|
||||
${CMAKE_COMMAND} -E tar xvf ../${package}.7z WORKING_DIRECTORY qtcreator)
|
||||
endforeach()
|
||||
|
||||
- name: Build
|
||||
shell: cmake -P {0}
|
||||
run: |
|
||||
@ -255,7 +249,7 @@ jobs:
|
||||
COMMAND python
|
||||
-u
|
||||
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
|
||||
--name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
||||
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}"
|
||||
--src .
|
||||
--build build
|
||||
--qt-path "${{ steps.qt.outputs.qt_dir }}"
|
||||
@ -271,19 +265,24 @@ jobs:
|
||||
endif()
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
||||
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||
|
||||
- name: Run unit tests
|
||||
if: startsWith(matrix.config.os, 'ubuntu')
|
||||
run: |
|
||||
xvfb-run ./build/build/test/QodeAssistTest
|
||||
|
||||
release:
|
||||
if: contains(github.ref, 'tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
path: release-with-dirs
|
||||
|
||||
@ -294,7 +293,7 @@ jobs:
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
7
.gitignore
vendored
@ -34,6 +34,7 @@ Thumbs.db
|
||||
*.rc
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
.qmlls.ini
|
||||
|
||||
# qtcreator generated files
|
||||
*.pro.user*
|
||||
@ -72,4 +73,8 @@ CMakeLists.txt.user*
|
||||
*.dll
|
||||
*.exe
|
||||
|
||||
/build
|
||||
/build
|
||||
/.qodeassist
|
||||
/.cursor
|
||||
/.vscode
|
||||
.qtc_clangd/compile_commands.json
|
||||
|
||||
0
.gitmodules
vendored
Normal file
3
.qmlformat.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[General]
|
||||
IndentWidth=4
|
||||
NewlineType=native
|
||||
134
CMakeLists.txt
@ -8,20 +8,59 @@ set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||
find_package(Qt6 COMPONENTS Widgets REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
|
||||
find_package(GTest)
|
||||
|
||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
||||
|
||||
# IDE_VERSION is defined by QtCreator package
|
||||
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
||||
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
|
||||
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(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(UIControls)
|
||||
add_subdirectory(ChatView)
|
||||
add_subdirectory(context)
|
||||
if(GTest_FOUND)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
add_qtc_plugin(QodeAssist
|
||||
PLUGIN_DEPENDS
|
||||
QtCreator::Core
|
||||
QtCreator::LanguageClient
|
||||
QtCreator::TextEditor
|
||||
QtCreator::ProjectExplorer
|
||||
QtCreator::CppEditor
|
||||
DEPENDS
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Widgets
|
||||
Qt::Network
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::ProjectExplorer
|
||||
QtCreator::CPlusPlus
|
||||
QodeAssistChatViewplugin
|
||||
SOURCES
|
||||
.github/workflows/build_cmake.yml
|
||||
.github/workflows/README.md
|
||||
@ -30,24 +69,89 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssistConstants.hpp
|
||||
QodeAssisttr.h
|
||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||
templates/PromptTemplate.hpp
|
||||
templates/CodeLLamaTemplate.hpp
|
||||
templates/StarCoder2Template.hpp
|
||||
templates/CodeQwenChat.hpp
|
||||
templates/DeepSeekCoderV2.hpp
|
||||
providers/LLMProvider.hpp
|
||||
templates/Templates.hpp
|
||||
templates/CodeLlamaFim.hpp
|
||||
templates/Ollama.hpp
|
||||
templates/Claude.hpp
|
||||
templates/OpenAI.hpp
|
||||
templates/MistralAI.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
# templates/DeepSeekCoderFim.hpp
|
||||
# templates/CustomFimTemplate.hpp
|
||||
templates/Qwen25CoderFIM.hpp
|
||||
templates/OpenAICompatible.hpp
|
||||
templates/Llama3.hpp
|
||||
templates/ChatML.hpp
|
||||
templates/Alpaca.hpp
|
||||
templates/Llama2.hpp
|
||||
templates/CodeLlamaQMLFim.hpp
|
||||
templates/GoogleAI.hpp
|
||||
templates/LlamaCppFim.hpp
|
||||
templates/Qwen3CoderFIM.hpp
|
||||
providers/Providers.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||
LLMProvidersManager.hpp LLMProvidersManager.cpp
|
||||
QodeAssistSettings.hpp QodeAssistSettings.cpp
|
||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||
QodeAssist.qrc
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||
QodeAssistHoverHandler.hpp QodeAssistHoverHandler.cpp
|
||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||
QodeAssistUtils.hpp
|
||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||
QodeAssistData.hpp
|
||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
||||
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
|
||||
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
|
||||
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
|
||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.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
|
||||
)
|
||||
|
||||
79
ChatView/CMakeLists.txt
Normal file
@ -0,0 +1,79 @@
|
||||
qt_add_library(QodeAssistChatView STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(QodeAssistChatView
|
||||
URI ChatView
|
||||
VERSION 1.0
|
||||
DEPENDENCIES
|
||||
QtQuick
|
||||
QML_FILES
|
||||
qml/RootItem.qml
|
||||
qml/ChatItem.qml
|
||||
qml/dialog/CodeBlock.qml
|
||||
qml/dialog/TextBlock.qml
|
||||
qml/parts/TopBar.qml
|
||||
qml/parts/BottomBar.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
|
||||
icons/attach-file-light.svg
|
||||
icons/attach-file-dark.svg
|
||||
icons/close-dark.svg
|
||||
icons/close-light.svg
|
||||
icons/link-file-light.svg
|
||||
icons/link-file-dark.svg
|
||||
icons/load-chat-dark.svg
|
||||
icons/save-chat-dark.svg
|
||||
icons/clean-icon-dark.svg
|
||||
icons/file-in-system.svg
|
||||
icons/window-lock.svg
|
||||
icons/window-unlock.svg
|
||||
icons/chat-icon.svg
|
||||
icons/chat-pause-icon.svg
|
||||
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
|
||||
ChatWidget.hpp ChatWidget.cpp
|
||||
ChatModel.hpp ChatModel.cpp
|
||||
ChatRootView.hpp ChatRootView.cpp
|
||||
ClientInterface.hpp ClientInterface.cpp
|
||||
MessagePart.hpp
|
||||
ChatUtils.h ChatUtils.cpp
|
||||
ChatSerializer.hpp ChatSerializer.cpp
|
||||
ChatView.hpp ChatView.cpp
|
||||
ChatData.hpp
|
||||
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
PUBLIC
|
||||
Qt::Widgets
|
||||
Qt::Quick
|
||||
Qt::QuickWidgets
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
32
ChatView/ChatData.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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
|
||||
537
ChatView/ChatModel.cpp
Normal file
@ -0,0 +1,537 @@
|
||||
/*
|
||||
* 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 "ChatModel.hpp"
|
||||
#include <utils/aspects.h>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtQml>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
connect(
|
||||
&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
|
||||
connect(&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditApplied,
|
||||
this,
|
||||
&ChatModel::onFileEditApplied);
|
||||
|
||||
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
|
||||
{
|
||||
return m_messages.size();
|
||||
}
|
||||
|
||||
QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_messages.size())
|
||||
return QVariant();
|
||||
|
||||
const Message &message = m_messages[index.row()];
|
||||
switch (static_cast<Roles>(role)) {
|
||||
case Roles::RoleType:
|
||||
return QVariant::fromValue(message.role);
|
||||
case Roles::Content: {
|
||||
return message.content;
|
||||
}
|
||||
case Roles::Attachments: {
|
||||
QStringList filenames;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
filenames << attachment.filename;
|
||||
}
|
||||
return filenames;
|
||||
}
|
||||
case Roles::IsRedacted: {
|
||||
return message.isRedacted;
|
||||
}
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[Roles::RoleType] = "roleType";
|
||||
roles[Roles::Content] = "content";
|
||||
roles[Roles::Attachments] = "attachments";
|
||||
roles[Roles::IsRedacted] = "isRedacted";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void ChatModel::addMessage(
|
||||
const QString &content,
|
||||
ChatRole role,
|
||||
const QString &id,
|
||||
const QList<Context::ContentFile> &attachments)
|
||||
{
|
||||
QString fullContent = content;
|
||||
if (!attachments.isEmpty()) {
|
||||
fullContent += "\n\nAttached files list:";
|
||||
for (const auto &attachment : attachments) {
|
||||
fullContent += QString("\nname: %1\nfile content:\n%2")
|
||||
.arg(attachment.filename, attachment.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
||||
&& m_messages.last().role == role) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
lastMessage.attachments = attachments;
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message newMessage{role, content, id};
|
||||
newMessage.attachments = attachments;
|
||||
m_messages.append(newMessage);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<ChatModel::Message> ChatModel::getChatHistory() const
|
||||
{
|
||||
return m_messages;
|
||||
}
|
||||
|
||||
void ChatModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_messages.clear();
|
||||
endResetModel();
|
||||
emit modelReseted();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
{
|
||||
QList<MessagePart> parts;
|
||||
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
||||
int lastIndex = 0;
|
||||
auto blockMatches = codeBlockRegex.globalMatch(content);
|
||||
|
||||
while (blockMatches.hasNext()) {
|
||||
auto match = blockMatches.next();
|
||||
if (match.capturedStart() > lastIndex) {
|
||||
QString textBetween
|
||||
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||
if (!textBetween.isEmpty()) {
|
||||
MessagePart part;
|
||||
part.type = MessagePartType::Text;
|
||||
part.text = textBetween;
|
||||
parts.append(part);
|
||||
}
|
||||
}
|
||||
|
||||
MessagePart codePart;
|
||||
codePart.type = MessagePartType::Code;
|
||||
codePart.text = match.captured(2).trimmed();
|
||||
codePart.language = match.captured(1);
|
||||
parts.append(codePart);
|
||||
|
||||
lastIndex = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastIndex < content.length()) {
|
||||
QString remainingText = content.mid(lastIndex).trimmed();
|
||||
|
||||
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
|
||||
{
|
||||
QJsonArray messages;
|
||||
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
|
||||
|
||||
for (const auto &message : m_messages) {
|
||||
QString role;
|
||||
switch (message.role) {
|
||||
case ChatRole::User:
|
||||
role = "user";
|
||||
break;
|
||||
case ChatRole::Assistant:
|
||||
role = "assistant";
|
||||
break;
|
||||
case ChatRole::Tool:
|
||||
case ChatRole::FileEdit:
|
||||
continue;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
QString content
|
||||
= message.attachments.isEmpty()
|
||||
? message.content
|
||||
: message.content + "\n\nAttached files list:"
|
||||
+ std::accumulate(
|
||||
message.attachments.begin(),
|
||||
message.attachments.end(),
|
||||
QString(),
|
||||
[](QString acc, const Context::ContentFile &attachment) {
|
||||
return acc
|
||||
+ QString("\nname: %1\nfile content:\n%2")
|
||||
.arg(attachment.filename, attachment.content);
|
||||
});
|
||||
|
||||
messages.append(QJsonObject{{"role", role}, {"content", content}});
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
QString ChatModel::lastMessageId() const
|
||||
{
|
||||
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
|
||||
114
ChatView/ChatModel.hpp
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 "ContextData.hpp"
|
||||
#include "MessagePart.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted };
|
||||
Q_ENUM(Roles)
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
QString id;
|
||||
bool isRedacted = false;
|
||||
QString signature = QString();
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
};
|
||||
|
||||
explicit ChatModel(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;
|
||||
|
||||
Q_INVOKABLE void addMessage(
|
||||
const QString &content,
|
||||
ChatRole role,
|
||||
const QString &id,
|
||||
const QList<Context::ContentFile> &attachments = {});
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
int tokensThreshold() const;
|
||||
|
||||
QString currentModel() 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:
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
|
||||
private slots:
|
||||
void onFileEditApplied(const QString &editId);
|
||||
void onFileEditRejected(const QString &editId);
|
||||
void onFileEditArchived(const QString &editId);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
||||
|
||||
QVector<Message> m_messages;
|
||||
bool m_loadingFromHistory = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||
1139
ChatView/ChatRootView.cpp
Normal file
215
ChatView/ChatRootView.hpp
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* 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 <QQuickItem>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "llmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
|
||||
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
|
||||
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
|
||||
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
|
||||
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
|
||||
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
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)
|
||||
Q_PROPERTY(bool isToolDebugging READ isToolDebugging NOTIFY isToolDebuggingChanged FINAL)
|
||||
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void showSaveDialog();
|
||||
Q_INVOKABLE void showLoadDialog();
|
||||
|
||||
void autosave();
|
||||
QString getAutosaveFilePath() const;
|
||||
|
||||
QStringList attachmentFiles() const;
|
||||
QStringList linkedFiles() const;
|
||||
|
||||
Q_INVOKABLE void showAttachFilesDialog();
|
||||
Q_INVOKABLE void removeFileFromAttachList(int index);
|
||||
Q_INVOKABLE void showLinkFilesDialog();
|
||||
Q_INVOKABLE void removeFileFromLinkList(int index);
|
||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
|
||||
bool isSyncOpenFiles() const;
|
||||
|
||||
void onEditorAboutToClose(Core::IEditor *editor);
|
||||
void onAppendLinkFileFromEditor(Core::IEditor *editor);
|
||||
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
|
||||
|
||||
QString chatFileName() const;
|
||||
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;
|
||||
|
||||
bool isToolDebugging() const;
|
||||
|
||||
public slots:
|
||||
void sendMessage(const QString &message);
|
||||
void copyToClipboard(const QString &text);
|
||||
void cancelRequest();
|
||||
void clearAttachmentFiles();
|
||||
void clearLinkedFiles();
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
void isSyncOpenFilesChanged();
|
||||
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();
|
||||
|
||||
void isToolDebuggingChanged();
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
LLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokensCount{0};
|
||||
int m_inputTokensCount{0};
|
||||
bool m_isSyncOpenFiles;
|
||||
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
|
||||
153
ChatView/ChatSerializer.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 "ChatSerializer.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
const QString ChatSerializer::VERSION = "0.1";
|
||||
|
||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||
{
|
||||
if (!ensureDirectoryExists(filePath)) {
|
||||
return {false, "Failed to create directory structure"};
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonObject root = serializeChat(model);
|
||||
QJsonDocument doc(root);
|
||||
|
||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QString version = root["version"].toString();
|
||||
|
||||
if (!validateVersion(version)) {
|
||||
return {false, QString("Unsupported version: %1").arg(version)};
|
||||
}
|
||||
|
||||
if (!deserializeChat(model, root)) {
|
||||
return {false, "Failed to deserialize chat data"};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
|
||||
{
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
messageObj["content"] = message.content;
|
||||
messageObj["id"] = message.id;
|
||||
messageObj["isRedacted"] = message.isRedacted;
|
||||
if (!message.signature.isEmpty()) {
|
||||
messageObj["signature"] = message.signature;
|
||||
}
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
message.content = json["content"].toString();
|
||||
message.id = json["id"].toString();
|
||||
message.isRedacted = json["isRedacted"].toBool(false);
|
||||
message.signature = json["signature"].toString();
|
||||
return message;
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
|
||||
{
|
||||
QJsonArray messagesArray;
|
||||
for (const auto &message : model->getChatHistory()) {
|
||||
messagesArray.append(serializeMessage(message));
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["version"] = VERSION;
|
||||
root["messages"] = messagesArray;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
||||
{
|
||||
QJsonArray messagesArray = json["messages"].toArray();
|
||||
QVector<ChatModel::Message> messages;
|
||||
messages.reserve(messagesArray.size());
|
||||
|
||||
for (const auto &messageValue : messagesArray) {
|
||||
messages.append(deserializeMessage(messageValue.toObject()));
|
||||
}
|
||||
|
||||
model->clear();
|
||||
|
||||
model->setLoadingFromHistory(true);
|
||||
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(message.content, message.role, message.id);
|
||||
}
|
||||
|
||||
model->setLoadingFromHistory(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QDir dir = fileInfo.dir();
|
||||
return dir.exists() || dir.mkpath(".");
|
||||
}
|
||||
|
||||
bool ChatSerializer::validateVersion(const QString &version)
|
||||
{
|
||||
return version == VERSION;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
56
ChatView/ChatSerializer.hpp
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
struct SerializationResult
|
||||
{
|
||||
bool success{false};
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class ChatSerializer
|
||||
{
|
||||
public:
|
||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
||||
|
||||
// Public for testing purposes
|
||||
static QJsonObject serializeMessage(const ChatModel::Message &message);
|
||||
static ChatModel::Message deserializeMessage(const QJsonObject &json);
|
||||
static QJsonObject serializeChat(const ChatModel *model);
|
||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
|
||||
|
||||
private:
|
||||
static const QString VERSION;
|
||||
static constexpr int CURRENT_VERSION = 1;
|
||||
|
||||
static bool ensureDirectoryExists(const QString &filePath);
|
||||
static bool validateVersion(const QString &version);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
68
ChatView/ChatUtils.cpp
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 "ChatUtils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
void ChatUtils::copyToClipboard(const QString &text)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
||||
QString ChatUtils::getSafeMarkdownText(const QString &text) const
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
bool needsSanitization = false;
|
||||
for (const QChar &ch : text) {
|
||||
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
|
||||
needsSanitization = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsSanitization) {
|
||||
return text;
|
||||
}
|
||||
|
||||
QString safeText;
|
||||
safeText.reserve(text.size());
|
||||
|
||||
for (QChar ch : text) {
|
||||
if (ch.isNull()) {
|
||||
safeText.append(' ');
|
||||
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
||||
safeText.append(ch);
|
||||
} else if (ch.isPrint()) {
|
||||
safeText.append(ch);
|
||||
} else {
|
||||
safeText.append(QChar(0xFFFD));
|
||||
}
|
||||
}
|
||||
|
||||
return safeText;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
40
ChatView/ChatUtils.h
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_NAMED_ELEMENT(ChatUtils)
|
||||
|
||||
public:
|
||||
explicit ChatUtils(QObject *parent = nullptr)
|
||||
: QObject(parent) {};
|
||||
|
||||
Q_INVOKABLE void copyToClipboard(const QString &text);
|
||||
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
106
ChatView/ChatView.cpp
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ChatView.hpp"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QSettings>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
namespace {
|
||||
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
||||
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
||||
| Qt::WindowCloseButtonHint;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatView::ChatView()
|
||||
: m_isPin(false)
|
||||
{
|
||||
setTitle("QodeAssist Chat");
|
||||
engine()->rootContext()->setContextProperty("_chatview", this);
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickView::SizeRootObjectToView);
|
||||
setMinimumSize({400, 300});
|
||||
setFlags(baseFlags);
|
||||
|
||||
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
|
||||
m_closeShortcut = new QShortcut(action->keySequence(), this);
|
||||
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
|
||||
|
||||
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
|
||||
if (m_closeShortcut) {
|
||||
m_closeShortcut->setKey(action->keySequence());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
restoreSettings();
|
||||
}
|
||||
|
||||
void ChatView::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
saveSettings();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ChatView::saveSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("QodeAssist/ChatView/geometry", geometry());
|
||||
settings.setValue("QodeAssist/ChatView/pinned", m_isPin);
|
||||
}
|
||||
|
||||
void ChatView::restoreSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
const QRect savedGeometry
|
||||
= settings.value("QodeAssist/ChatView/geometry", QRect(100, 100, 800, 600)).toRect();
|
||||
setGeometry(savedGeometry);
|
||||
|
||||
const bool pinned = settings.value("QodeAssist/ChatView/pinned", false).toBool();
|
||||
setIsPin(pinned);
|
||||
}
|
||||
|
||||
bool ChatView::isPin() const
|
||||
{
|
||||
return m_isPin;
|
||||
}
|
||||
|
||||
void ChatView::setIsPin(bool newIsPin)
|
||||
{
|
||||
if (m_isPin == newIsPin)
|
||||
return;
|
||||
m_isPin = newIsPin;
|
||||
|
||||
if (m_isPin) {
|
||||
setFlags(baseFlags | Qt::WindowStaysOnTopHint);
|
||||
} else {
|
||||
setFlags(baseFlags);
|
||||
}
|
||||
|
||||
emit isPinChanged();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
51
ChatView/ChatView.hpp
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickView>
|
||||
#include <QShortcut>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatView : public QQuickView
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
||||
public:
|
||||
ChatView();
|
||||
|
||||
bool isPin() const;
|
||||
void setIsPin(bool newIsPin);
|
||||
|
||||
signals:
|
||||
void isPinChanged();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
void saveSettings();
|
||||
void restoreSettings();
|
||||
|
||||
bool m_isPin;
|
||||
QShortcut *m_closeShortcut;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
43
ChatView/ChatWidget.cpp
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 "ChatWidget.hpp"
|
||||
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
{
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
}
|
||||
|
||||
void ChatWidget::clear()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||
}
|
||||
|
||||
void ChatWidget::scrollToBottom()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||
}
|
||||
} // namespace QodeAssist::Chat
|
||||
41
ChatView/ChatWidget.hpp
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 <QtQuickWidgets/QtQuickWidgets>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatWidget : public QQuickWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatWidget(QWidget *parent = nullptr);
|
||||
~ChatWidget() = default;
|
||||
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE void scrollToBottom();
|
||||
|
||||
signals:
|
||||
void clearPressed();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
377
ChatView/ClientInterface.cpp
Normal file
@ -0,0 +1,377 @@
|
||||
/*
|
||||
* 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 "ClientInterface.hpp"
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/editormanager/ieditor.h>
|
||||
#include <coreplugin/idocument.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "RequestConfig.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
#include <RulesLoader.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
{
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
void ClientInterface::sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments,
|
||||
const QList<QString> &linkedFiles,
|
||||
bool useAgentMode)
|
||||
{
|
||||
cancelRequest();
|
||||
m_accumulatedResponses.clear();
|
||||
|
||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||
|
||||
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::ContextData context;
|
||||
|
||||
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
||||
|
||||
if (chatAssistantSettings.useSystemPrompt()) {
|
||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||
|
||||
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;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LLMCore::Message apiMessage;
|
||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||
apiMessage.isRedacted = msg.isRedacted;
|
||||
apiMessage.signature = msg.signature;
|
||||
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
context.history = messages;
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3")
|
||||
.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.provider->prepareRequest(
|
||||
config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat, isToolsEnabled);
|
||||
|
||||
QString requestId = QUuid::createUuid().toString();
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
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()
|
||||
{
|
||||
m_chatModel->clear();
|
||||
LOG_MESSAGE("Chat history cleared");
|
||||
}
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
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)
|
||||
{
|
||||
const auto message = response.trimmed();
|
||||
|
||||
if (!message.isEmpty()) {
|
||||
QString messageId = request["id"].toString();
|
||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
QString ClientInterface::getCurrentFileContext() const
|
||||
{
|
||||
auto currentEditor = Core::EditorManager::currentEditor();
|
||||
if (!currentEditor) {
|
||||
LOG_MESSAGE("No active editor found");
|
||||
return QString();
|
||||
}
|
||||
|
||||
auto textDocument = qobject_cast<TextEditor::TextDocument *>(currentEditor->document());
|
||||
if (!textDocument) {
|
||||
LOG_MESSAGE("Current document is not a text document");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
|
||||
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
|
||||
|
||||
QString content = textDocument->document()->toPlainText();
|
||||
|
||||
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
|
||||
|
||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||
}
|
||||
|
||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
||||
{
|
||||
QString updatedPrompt = basePrompt;
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||
|
||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||
for (const auto &file : contentFiles) {
|
||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
83
ChatView/ClientInterface.hpp
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "llmcore/IPromptProvider.hpp"
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ClientInterface : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {},
|
||||
bool useAgentMode = false);
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &error);
|
||||
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:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
QHash<QString, QString> m_accumulatedResponses;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
42
ChatView/MessagePart.hpp
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "ChatData.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class MessagePart
|
||||
{
|
||||
Q_GADGET
|
||||
Q_PROPERTY(MessagePartType type MEMBER type CONSTANT FINAL)
|
||||
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
|
||||
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
|
||||
QML_VALUE_TYPE(messagePart)
|
||||
public:
|
||||
MessagePartType type;
|
||||
QString text;
|
||||
QString language;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
15
ChatView/icons/apply-changes-button.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 548 B |
11
ChatView/icons/attach-file-dark.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_37_14)">
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_37_14">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
11
ChatView/icons/attach-file-light.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_20)">
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_20">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 869 B |
10
ChatView/icons/chat-icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_6)">
|
||||
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_6">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 523 B |
10
ChatView/icons/chat-pause-icon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5_17)">
|
||||
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8ZM8.4 6H15.6V13.2H8.4V6Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5_17">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 548 B |
8
ChatView/icons/clean-icon-dark.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.75 15H3.25C2.00736 15 1 16.0074 1 17.25V39.75C1 40.9926 2.00736 42 3.25 42H16.75C17.9926 42 19 40.9926 19 39.75V17.25C19 16.0074 17.9926 15 16.75 15Z" stroke="black" stroke-width="2"/>
|
||||
<path d="M1.04316 11.015L18.9554 8.90787" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7.19462 10.363L7.02032 8.59516C6.92446 7.62284 8.18688 6.64116 9.8257 6.41365C11.4645 6.18615 12.8838 6.79555 12.9797 7.76787L13.154 9.53573" stroke="black" stroke-width="2"/>
|
||||
<path d="M6 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M10 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M14 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 822 B |
10
ChatView/icons/close-dark.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_41_14)">
|
||||
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_41_14">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
10
ChatView/icons/close-light.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_41_14)">
|
||||
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_41_14">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
12
ChatView/icons/file-in-system.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_59_114)">
|
||||
<path d="M2 8H12L16 4H40C42 4 44 6 44 8V36C44 38 42 40 40 40H6C4 40 2 38 2 36V8Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="3"/>
|
||||
<path d="M25 37C32.732 37 39 30.732 39 23C39 15.268 32.732 9 25 9C17.268 9 11 15.268 11 23C11 30.732 17.268 37 25 37Z" stroke="black" stroke-width="4"/>
|
||||
<path d="M33 35L42 44" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_59_114">
|
||||
<rect width="44" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 624 B |
12
ChatView/icons/link-file-dark.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_49_24)">
|
||||
<path d="M10 12L10 32L10 12Z" fill="black"/>
|
||||
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
|
||||
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_49_24">
|
||||
<rect width="20" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 513 B |
12
ChatView/icons/link-file-light.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_51_24)">
|
||||
<path d="M10 12L10 32Z" fill="white"/>
|
||||
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
|
||||
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_51_24">
|
||||
<rect width="20" height="44" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
5
ChatView/icons/load-chat-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M10 16V36" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M5 21L10 16L15 21" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
17
ChatView/icons/open-in-editor.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 943 B |
16
ChatView/icons/reject-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 599 B |
9
ChatView/icons/rules-icon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
ChatView/icons/save-chat-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 8V28" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M5 23L10 28L15 23" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 36H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
4
ChatView/icons/thinking-icon-off.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
3
ChatView/icons/thinking-icon-on.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
16
ChatView/icons/undo-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 663 B |
5
ChatView/icons/window-lock.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="4"/>
|
||||
<path d="M14 18V10C14 5.6 17.6 2 22 2C26.4 2 30 5.6 30 10V18" stroke="black" stroke-width="4"/>
|
||||
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 552 B |
5
ChatView/icons/window-unlock.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="4"/>
|
||||
<path d="M14 17V9.5C14 5.375 17.15 2 21 2C24.85 2 27.5 2.875 27.5 7" stroke="black" stroke-width="4"/>
|
||||
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 559 B |
220
ChatView/qml/ChatItem.qml
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import ChatView
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import UIControls
|
||||
|
||||
import "./dialog"
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias msgModel: msgCreator.model
|
||||
property alias messageAttachments: attachmentsModel.model
|
||||
property string textFontFamily: Qt.application.font.family
|
||||
property string codeFontFamily: {
|
||||
switch (Qt.platform.os) {
|
||||
case "windows":
|
||||
return "Consolas";
|
||||
case "osx":
|
||||
return "Menlo";
|
||||
case "linux":
|
||||
return "DejaVu Sans Mono";
|
||||
default:
|
||||
return "monospace";
|
||||
}
|
||||
}
|
||||
property int textFontSize: Qt.application.font.pointSize
|
||||
property int codeFontSize: Qt.application.font.pointSize
|
||||
property int textFormat: 0
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
color: isUserMessage ? palette.alternateBase
|
||||
: palette.base
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: msgColumn
|
||||
|
||||
x: 5
|
||||
width: parent.width - x
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: msgCreator
|
||||
delegate: Loader {
|
||||
id: msgCreatorDelegate
|
||||
// Fix me:
|
||||
// why does `required property MessagePart modelData` not work?
|
||||
required property var modelData
|
||||
|
||||
Layout.preferredWidth: root.width
|
||||
sourceComponent: {
|
||||
// If `required property MessagePart modelData` is used
|
||||
// and conversion to MessagePart fails, you're left
|
||||
// with a nullptr. This tests that to prevent crashing.
|
||||
if(!modelData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch(modelData.type) {
|
||||
case MessagePartType.Text: return textComponent;
|
||||
case MessagePartType.Code: return codeBlockComponent;
|
||||
default: return textComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComponent
|
||||
TextComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
CodeBlockComponent {
|
||||
itemData: msgCreatorDelegate.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: attachmentsFlow
|
||||
|
||||
Layout.fillWidth: true
|
||||
visible: attachmentsModel.model && attachmentsModel.model.length > 0
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
spacing: 5
|
||||
|
||||
Repeater {
|
||||
id: attachmentsModel
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
height: attachText.implicitHeight + 8
|
||||
width: attachText.implicitWidth + 16
|
||||
radius: 4
|
||||
color: palette.button
|
||||
border.width: 1
|
||||
border.color: palette.mid
|
||||
|
||||
Text {
|
||||
id: attachText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
required property var itemData
|
||||
height: implicitHeight + 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 10
|
||||
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||
: itemData.text
|
||||
font.family: root.textFontFamily
|
||||
font.pointSize: root.textFontSize
|
||||
textFormat: {
|
||||
if (root.textFormat == 0) {
|
||||
return Text.MarkdownText
|
||||
} else if (root.textFormat == 1) {
|
||||
return Text.RichText
|
||||
} else {
|
||||
return Text.PlainText
|
||||
}
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
}
|
||||
|
||||
component CodeBlockComponent : CodeBlock {
|
||||
id: codeblock
|
||||
|
||||
required property var itemData
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
code: itemData.text
|
||||
language: itemData.language
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
}
|
||||
}
|
||||
469
ChatView/qml/FileEditItem.qml
Normal file
@ -0,0 +1,469 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
484
ChatView/qml/RootItem.qml
Normal file
@ -0,0 +1,484 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic as QQC
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
import "./parts"
|
||||
|
||||
ChatRootView {
|
||||
id: root
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: palette.window
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
TopBar {
|
||||
id: topBar
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height + 10
|
||||
|
||||
saveButton.onClicked: root.showSaveDialog()
|
||||
loadButton.onClicked: root.showLoadDialog()
|
||||
clearButton.onClicked: root.clearChat()
|
||||
tokensBadge {
|
||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
}
|
||||
recentPath {
|
||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
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 {
|
||||
id: chatListView
|
||||
|
||||
signal hideServiceComponents(int itemIndex)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
leftMargin: 5
|
||||
model: root.chatModel
|
||||
clip: true
|
||||
spacing: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
cacheBuffer: 2000
|
||||
|
||||
delegate: Loader {
|
||||
id: componentLoader
|
||||
|
||||
required property var model
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width - scroll.width
|
||||
|
||||
sourceComponent: {
|
||||
if (model.roleType === ChatModel.Tool) {
|
||||
return toolMessageComponent
|
||||
} else if (model.roleType === ChatModel.FileEdit) {
|
||||
return fileEditMessageComponent
|
||||
} else if (model.roleType === ChatModel.Thinking) {
|
||||
return thinkingMessageComponent
|
||||
} else {
|
||||
return chatItemComponent
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (componentLoader.sourceComponent == chatItemComponent && !root.isToolDebugging) {
|
||||
chatListView.hideServiceComponents(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: Item {
|
||||
width: ListView.view.width - scroll.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
ScrollBar.vertical: QQC.ScrollBar {
|
||||
id: scroll
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
root.scrollToBottom()
|
||||
}
|
||||
|
||||
onContentHeightChanged: {
|
||||
if (atYEnd) {
|
||||
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
|
||||
|
||||
FadeListItemAnimation{
|
||||
id: toolFadeAnimation
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
toolFadeAnimation.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
FadeListItemAnimation{
|
||||
id: thinkingFadeAnimation
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: chatListView
|
||||
function onHideServiceComponents(itemIndex) {
|
||||
if (index !== itemIndex) {
|
||||
thinkingFadeAnimation.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 30
|
||||
Layout.maximumHeight: root.height / 2
|
||||
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: Qt.platform.os === "osx"
|
||||
? qsTr("Type your message here... (⌘+↩ to send)")
|
||||
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
color: palette.base
|
||||
border.color: messageInput.activeFocus ? palette.highlight : palette.button
|
||||
border.width: 1
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: palette.highlight
|
||||
opacity: messageInput.hovered ? 0.1 : 0
|
||||
radius: parent.radius
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: messageContextMenu.open()
|
||||
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 {
|
||||
id: attachedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.attachmentFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
|
||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
|
||||
}
|
||||
|
||||
AttachedFilesPlace {
|
||||
id: linkedFilesPlace
|
||||
|
||||
Layout.fillWidth: true
|
||||
attachedFilesModel: root.linkedFiles
|
||||
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
|
||||
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
|
||||
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 {
|
||||
id: bottomBar
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: 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 {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
}
|
||||
attachFiles.onClicked: root.showAttachFilesDialog()
|
||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: sendMessageShortcut
|
||||
|
||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
||||
root.sendChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.chatModel.clear()
|
||||
root.clearAttachmentFiles()
|
||||
root.updateInputTokensCount()
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text)
|
||||
messageInput.text = ""
|
||||
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()
|
||||
}
|
||||
}
|
||||
181
ChatView/qml/ThinkingStatusItem.qml
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
160
ChatView/qml/ToolStatusItem.qml
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
173
ChatView/qml/dialog/CodeBlock.qml
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import UIControls
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
property bool expanded: false
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
readonly property real collapsedHeight: copyButton.height + 10
|
||||
|
||||
color: palette.alternateBase
|
||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||
: Qt.lighter(root.color, 1.3)
|
||||
border.width: 2
|
||||
radius: 4
|
||||
implicitWidth: parent.width
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
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 {
|
||||
id: codeText
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: header.bottom
|
||||
margins: 10
|
||||
}
|
||||
text: root.code
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: contextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Platform.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Platform.MenuItem {
|
||||
text: qsTr("Copy")
|
||||
onTriggered: {
|
||||
const textToCopy = codeText.selectedText || root.code
|
||||
utils.copyToClipboard(textToCopy)
|
||||
}
|
||||
}
|
||||
|
||||
Platform.MenuSeparator {}
|
||||
|
||||
Platform.MenuItem {
|
||||
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||
onTriggered: root.expanded = !root.expanded
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: copyButton
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
|
||||
y: 5
|
||||
text: qsTr("Copy")
|
||||
|
||||
onClicked: {
|
||||
utils.copyToClipboard(root.code)
|
||||
text = qsTr("Copied")
|
||||
copyTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
interval: 2000
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
56
ChatView/qml/dialog/TextBlock.qml
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
TextEdit {
|
||||
id: root
|
||||
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
wrapMode: Text.WordWrap
|
||||
selectionColor: palette.highlight
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
109
ChatView/qml/parts/AttachedFilesPlace.qml
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
property alias attachedFilesModel: attachRepeater.model
|
||||
property color accentColor: palette.mid
|
||||
property string iconPath
|
||||
|
||||
signal removeFileFromListByIndex(index: int)
|
||||
|
||||
spacing: 5
|
||||
leftPadding: 5
|
||||
rightPadding: 5
|
||||
topPadding: attachRepeater.model.length > 0 ? 2 : 0
|
||||
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
|
||||
|
||||
Repeater {
|
||||
id: attachRepeater
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
height: 30
|
||||
width: contentRow.width + 10
|
||||
radius: 4
|
||||
color: palette.button
|
||||
border.width: 1
|
||||
border.color: mouse.hovered ? palette.highlight : root.accentColor
|
||||
|
||||
HoverHandler {
|
||||
id: mouse
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
|
||||
spacing: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: root.iconPath
|
||||
sourceSize.width: 8
|
||||
sourceSize.height: 15
|
||||
}
|
||||
|
||||
Text {
|
||||
id: fileNameText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: palette.buttonText
|
||||
|
||||
text: {
|
||||
const parts = modelData.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeButton
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: closeIcon.width + 5
|
||||
height: closeButton.width + 5
|
||||
|
||||
onClicked: root.removeFileFromListByIndex(index)
|
||||
|
||||
Image {
|
||||
id: closeIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
|
||||
width: 6
|
||||
height: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
ChatView/qml/parts/BottomBar.qml
Normal file
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias sendButton: sendButtonId
|
||||
property alias syncOpenFiles: syncOpenFilesId
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias linkFiles: linkFilesId
|
||||
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
RowLayout {
|
||||
id: bottomBar
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
icon {
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: attachFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Attach file to message")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: linkFilesId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||
height: 15
|
||||
width: 8
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Link file to context")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: syncOpenFilesId
|
||||
|
||||
text: qsTr("Sync open files")
|
||||
|
||||
ToolTip.visible: syncOpenFilesId.hovered
|
||||
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
161
ChatView/qml/parts/FileEditsActionBar.qml
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
251
ChatView/qml/parts/RulesViewer.qml
Normal file
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
103
ChatView/qml/parts/Toast.qml
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
243
ChatView/qml/parts/TopBar.qml
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property alias saveButton: saveButtonId
|
||||
property alias loadButton: loadButtonId
|
||||
property alias clearButton: clearButtonId
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
property alias recentPath: recentPathId
|
||||
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 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
|
||||
Flow {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
margins: 5
|
||||
}
|
||||
spacing: 10
|
||||
|
||||
Row {
|
||||
height: agentModeSwitchId.height
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: pinButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checkable: true
|
||||
|
||||
icon {
|
||||
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: checked ? qsTr("Unpin chat window")
|
||||
: qsTr("Pin chat window to the top")
|
||||
}
|
||||
|
||||
QoATextSlider {
|
||||
id: agentModeSwitchId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
leftText: "chat"
|
||||
rightText: "AI Agent"
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
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 {
|
||||
id: thinkingModeId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
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 {
|
||||
height: agentModeSwitchId.height
|
||||
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 {
|
||||
Layout.preferredWidth: root.width
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
CodeHandler.cpp
Normal file
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||
*
|
||||
* 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 "CodeHandler.hpp"
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
struct LanguageProperties
|
||||
{
|
||||
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;
|
||||
QStringList lines = text.split('\n');
|
||||
bool inCodeBlock = false;
|
||||
QString pendingComments;
|
||||
|
||||
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) {
|
||||
if (line.trimmed().startsWith("```")) {
|
||||
if (!inCodeBlock) {
|
||||
auto lineLanguage = detectLanguageFromLine(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;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
result += line + "\n";
|
||||
} else {
|
||||
QString trimmed = line.trimmed();
|
||||
if (!trimmed.isEmpty()) {
|
||||
pendingComments += trimmed + "\n";
|
||||
} else {
|
||||
pendingComments += "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPendingCommentsIfAny();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CodeHandler::getCommentPrefix(const QString &language)
|
||||
{
|
||||
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
|
||||
return commentPrefixes.value(language, "//");
|
||||
}
|
||||
|
||||
QString CodeHandler::detectLanguageFromLine(const QString &line)
|
||||
{
|
||||
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
|
||||
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
|
||||
}
|
||||
|
||||
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
|
||||
{
|
||||
static const auto extensionToLanguage = buildExtensionToLanguageMap();
|
||||
return extensionToLanguage.value(extension.toLower(), "");
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
||||
{
|
||||
static const QRegularExpression
|
||||
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
|
||||
{
|
||||
static const QRegularExpression
|
||||
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
|
||||
{
|
||||
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
56
CodeHandler.hpp
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class CodeHandler
|
||||
{
|
||||
public:
|
||||
static QString processText(QString text, QString currentFileName);
|
||||
|
||||
/**
|
||||
* 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:
|
||||
static QString getCommentPrefix(const QString &language);
|
||||
|
||||
static const QRegularExpression &getFullCodeBlockRegex();
|
||||
static const QRegularExpression &getPartialStartBlockRegex();
|
||||
static const QRegularExpression &getPartialEndBlockRegex();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
231
ConfigurationManager.cpp
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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 "ConfigurationManager.hpp"
|
||||
|
||||
#include <settings/ButtonAspect.hpp>
|
||||
#include <QTimer>
|
||||
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void ConfigurationManager::init()
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
||||
{
|
||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
||||
}
|
||||
|
||||
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)
|
||||
: QObject(parent)
|
||||
, m_generalSettings(Settings::generalSettings())
|
||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
void ConfigurationManager::setupConnections()
|
||||
{
|
||||
using Config = ConfigurationManager;
|
||||
using Button = ButtonAspect;
|
||||
|
||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||
|
||||
connect(
|
||||
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
||||
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
|
||||
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
||||
connect(
|
||||
&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);
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectProvider()
|
||||
{
|
||||
const auto providersList = m_providersManager.providersNames();
|
||||
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
||||
? m_generalSettings.ccProvider
|
||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
||||
? m_generalSettings.ccPreset1Provider
|
||||
: m_generalSettings.caProvider;
|
||||
|
||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectModel()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
||||
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
|
||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: m_generalSettings.caModel;
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->supportsModelListing()) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
||||
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectTemplate()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
||||
: m_generalSettings.caProvider.volatileValue();
|
||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
||||
|
||||
const auto templateList = isCodeCompletion || isPreset1
|
||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
||||
: m_generalSettings.caTemplate;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigurationManager::selectUrl()
|
||||
{
|
||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
||||
if (!settingsButton)
|
||||
return;
|
||||
|
||||
QStringList urls;
|
||||
for (const auto &name : m_providersManager.providersNames()) {
|
||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
||||
if (!urls.contains(url))
|
||||
urls.append(url);
|
||||
}
|
||||
|
||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
||||
? m_generalSettings.ccPreset1Url
|
||||
: m_generalSettings.caUrl;
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
63
ConfigurationManager.hpp
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 <QObject>
|
||||
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static ConfigurationManager &instance();
|
||||
|
||||
void init();
|
||||
|
||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
||||
void updateAllTemplateDescriptions();
|
||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
||||
void checkAllTemplate();
|
||||
|
||||
public slots:
|
||||
void selectProvider();
|
||||
void selectModel();
|
||||
void selectTemplate();
|
||||
void selectUrl();
|
||||
|
||||
private:
|
||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
||||
~ConfigurationManager() = default;
|
||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||
|
||||
Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::ProvidersManager &m_providersManager;
|
||||
LLMCore::PromptTemplateManager &m_templateManger;
|
||||
|
||||
void setupConnections();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,212 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "DocumentContextReader.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QTextBlock>
|
||||
#include <languageserverprotocol/lsptypes.h>
|
||||
|
||||
#include "QodeAssistSettings.hpp"
|
||||
|
||||
const QRegularExpression &getYearRegex()
|
||||
{
|
||||
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
|
||||
return yearRegex;
|
||||
}
|
||||
|
||||
const QRegularExpression &getNameRegex()
|
||||
{
|
||||
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
|
||||
return nameRegex;
|
||||
}
|
||||
|
||||
const QRegularExpression &getCommentRegex()
|
||||
{
|
||||
static const QRegularExpression
|
||||
commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
|
||||
QRegularExpression::MultilineOption);
|
||||
return commentRegex;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
|
||||
: m_textDocument(textDocument)
|
||||
, m_document(textDocument->document())
|
||||
{
|
||||
m_copyrightInfo = findCopyright();
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
if (!m_document || lineNumber < 0)
|
||||
return QString();
|
||||
|
||||
QTextBlock block = m_document->begin();
|
||||
int currentLine = 0;
|
||||
|
||||
while (block.isValid()) {
|
||||
if (currentLine == lineNumber) {
|
||||
QString text = block.text();
|
||||
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
|
||||
text = text.left(cursorPosition);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
currentLine++;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getContextBefore(int lineNumber,
|
||||
int cursorPosition,
|
||||
int linesCount) const
|
||||
{
|
||||
int effectiveStartLine;
|
||||
if (m_copyrightInfo.found) {
|
||||
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
|
||||
} else {
|
||||
effectiveStartLine = qMax(0, lineNumber - linesCount);
|
||||
}
|
||||
|
||||
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getContextAfter(int lineNumber,
|
||||
int cursorPosition,
|
||||
int linesCount) const
|
||||
{
|
||||
int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
|
||||
return getContextBetween(lineNumber + 1, endLine, cursorPosition);
|
||||
}
|
||||
|
||||
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
int startLine = 0;
|
||||
if (m_copyrightInfo.found) {
|
||||
startLine = m_copyrightInfo.endLine + 1;
|
||||
}
|
||||
|
||||
startLine = qMin(startLine, lineNumber);
|
||||
|
||||
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
|
||||
{
|
||||
return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getLanguageAndFileInfo() const
|
||||
{
|
||||
if (!m_textDocument)
|
||||
return QString();
|
||||
|
||||
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
|
||||
m_textDocument->mimeType());
|
||||
QString mimeType = m_textDocument->mimeType();
|
||||
QString filePath = m_textDocument->filePath().toString();
|
||||
QString fileExtension = QFileInfo(filePath).suffix();
|
||||
|
||||
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
|
||||
.arg(language, mimeType, filePath, fileExtension);
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getSpecificInstructions() const
|
||||
{
|
||||
QString specificInstruction = settings().specificInstractions().arg(
|
||||
LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_textDocument->mimeType()));
|
||||
return QString("Instructions: %1").arg(specificInstruction);
|
||||
}
|
||||
|
||||
CopyrightInfo DocumentContextReader::findCopyright()
|
||||
{
|
||||
CopyrightInfo result = {-1, -1, false};
|
||||
|
||||
QString text = m_document->toPlainText();
|
||||
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
|
||||
|
||||
QList<CopyrightInfo> copyrightBlocks;
|
||||
|
||||
while (matchIterator.hasNext()) {
|
||||
QRegularExpressionMatch match = matchIterator.next();
|
||||
QString matchedText = match.captured().toLower();
|
||||
|
||||
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|
||||
|| matchedText.contains("(c)") || matchedText.contains("©")
|
||||
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
|
||||
int startPos = match.capturedStart();
|
||||
int endPos = match.capturedEnd();
|
||||
|
||||
CopyrightInfo info;
|
||||
info.startLine = m_document->findBlock(startPos).blockNumber();
|
||||
info.endLine = m_document->findBlock(endPos).blockNumber();
|
||||
info.found = true;
|
||||
|
||||
copyrightBlocks.append(info);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
|
||||
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
|
||||
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
|
||||
copyrightBlocks.removeAt(i + 1);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
|
||||
return copyrightBlocks.first();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString DocumentContextReader::getContextBetween(int startLine,
|
||||
int endLine,
|
||||
int cursorPosition) const
|
||||
{
|
||||
QString context;
|
||||
for (int i = startLine; i <= endLine; ++i) {
|
||||
QTextBlock block = m_document->findBlockByNumber(i);
|
||||
if (!block.isValid()) {
|
||||
break;
|
||||
}
|
||||
if (i == endLine) {
|
||||
context += block.text().left(cursorPosition);
|
||||
} else {
|
||||
context += block.text() + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
CopyrightInfo DocumentContextReader::copyrightInfo() const
|
||||
{
|
||||
return m_copyrightInfo;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <QTextDocument>
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
struct CopyrightInfo
|
||||
{
|
||||
int startLine;
|
||||
int endLine;
|
||||
bool found;
|
||||
};
|
||||
|
||||
class DocumentContextReader
|
||||
{
|
||||
public:
|
||||
DocumentContextReader(TextEditor::TextDocument *textDocument);
|
||||
|
||||
QString getLineText(int lineNumber, int cursorPosition = -1) const;
|
||||
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
|
||||
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
|
||||
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
|
||||
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
|
||||
QString getLanguageAndFileInfo() const;
|
||||
QString getSpecificInstructions() const;
|
||||
CopyrightInfo findCopyright();
|
||||
QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
|
||||
|
||||
CopyrightInfo copyrightInfo() const;
|
||||
|
||||
private:
|
||||
TextEditor::TextDocument *m_textDocument;
|
||||
QTextDocument *m_document;
|
||||
CopyrightInfo m_copyrightInfo;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -23,25 +23,42 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
|
||||
#include "DocumentContextReader.hpp"
|
||||
#include "LLMProvidersManager.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "QodeAssistSettings.hpp"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "context/Utils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <llmcore/RulesLoader.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMClientInterface::LLMClientInterface()
|
||||
: m_manager(new QNetworkAccessManager(this))
|
||||
LLMClientInterface::LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
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))
|
||||
{
|
||||
updateProvider();
|
||||
}
|
||||
|
||||
LLMClientInterface::~LLMClientInterface()
|
||||
{
|
||||
handleCancelRequest();
|
||||
}
|
||||
|
||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||
{
|
||||
return "Qode Assist";
|
||||
return "QodeAssist";
|
||||
}
|
||||
|
||||
void LLMClientInterface::startImpl()
|
||||
@ -49,10 +66,31 @@ void LLMClientInterface::startImpl()
|
||||
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));
|
||||
m_activeRequests.erase(it);
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendData(const QByteArray &data)
|
||||
{
|
||||
updateProvider();
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject())
|
||||
return;
|
||||
@ -70,98 +108,40 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
handleTextDocumentDidOpen(request);
|
||||
} else if (method == "getCompletionsCycling") {
|
||||
QString requestId = request["id"].toString();
|
||||
startTimeMeasurement(requestId);
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
handleCompletion(request);
|
||||
} else if (method == "$/cancelRequest") {
|
||||
handleCancelRequest(request);
|
||||
handleCancelRequest();
|
||||
} else if (method == "exit") {
|
||||
// TODO make exit handler
|
||||
} else {
|
||||
logMessage(QString("Unknown method: %1").arg(method));
|
||||
LOG_MESSAGE(QString("Unknown method: %1").arg(method));
|
||||
}
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
||||
void LLMClientInterface::handleCancelRequest()
|
||||
{
|
||||
QString id = request["params"].toObject()["id"].toString();
|
||||
if (m_activeRequests.contains(id)) {
|
||||
m_activeRequests[id]->abort();
|
||||
m_activeRequests.remove(id);
|
||||
logMessage(QString("Request %1 cancelled successfully").arg(id));
|
||||
} else {
|
||||
logMessage(QString("Request %1 not found").arg(id));
|
||||
}
|
||||
}
|
||||
|
||||
bool LLMClientInterface::processSingleLineCompletion(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedCompletion)
|
||||
{
|
||||
int newlinePos = accumulatedCompletion.indexOf('\n');
|
||||
|
||||
if (newlinePos != -1) {
|
||||
QString singleLineCompletion = accumulatedCompletion.left(newlinePos).trimmed();
|
||||
singleLineCompletion = removeStopWords(singleLineCompletion);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
sendCompletionToClient(singleLineCompletion, request, position, true);
|
||||
m_accumulatedResponses.remove(reply);
|
||||
reply->abort();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString LLMClientInterface::сontextBefore(TextEditor::TextEditorWidget *widget,
|
||||
int lineNumber,
|
||||
int cursorPosition)
|
||||
{
|
||||
if (!widget)
|
||||
return QString();
|
||||
|
||||
DocumentContextReader reader(widget->textDocument());
|
||||
const auto ©right = reader.copyrightInfo();
|
||||
|
||||
logMessage(QString{"Line Number: %1"}.arg(lineNumber));
|
||||
logMessage(QString("Copyright found %1 %2").arg(copyright.found).arg(copyright.endLine));
|
||||
if (lineNumber < reader.findCopyright().endLine)
|
||||
return QString();
|
||||
|
||||
QString contextBefore;
|
||||
if (settings().readFullFile()) {
|
||||
contextBefore = reader.readWholeFileBefore(lineNumber, cursorPosition);
|
||||
} else {
|
||||
contextBefore = reader.getContextBefore(lineNumber,
|
||||
cursorPosition,
|
||||
settings().readStringsBeforeCursor());
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
return contextBefore;
|
||||
}
|
||||
|
||||
QString LLMClientInterface::сontextAfter(TextEditor::TextEditorWidget *widget,
|
||||
int lineNumber,
|
||||
int cursorPosition)
|
||||
{
|
||||
if (!widget)
|
||||
return QString();
|
||||
|
||||
DocumentContextReader reader(widget->textDocument());
|
||||
if (lineNumber < reader.findCopyright().endLine)
|
||||
return QString();
|
||||
|
||||
QString contextAfter;
|
||||
if (settings().readFullFile()) {
|
||||
contextAfter = reader.readWholeFileAfter(lineNumber, cursorPosition);
|
||||
} else {
|
||||
contextAfter = reader.getContextAfter(lineNumber,
|
||||
cursorPosition,
|
||||
settings().readStringsAfterCursor());
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider, nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
return contextAfter;
|
||||
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)
|
||||
@ -214,102 +194,235 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleLLMResponse(QNetworkReply *reply, const QJsonObject &request)
|
||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
{
|
||||
QString &accumulatedResponse = m_accumulatedResponses[reply];
|
||||
|
||||
auto &templateManager = PromptTemplateManager::instance();
|
||||
const Templates::PromptTemplate *currentTemplate = templateManager.getCurrentTemplate();
|
||||
|
||||
auto &providerManager = LLMProvidersManager::instance();
|
||||
bool isComplete = providerManager.getCurrentProvider()->handleResponse(reply,
|
||||
accumulatedResponse);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
if (!settings().multiLineCompletion()
|
||||
&& processSingleLineCompletion(reply, request, accumulatedResponse)) {
|
||||
auto filePath = Context::extractFilePathFromRequest(request);
|
||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||
if (!documentInfo.document) {
|
||||
LOG_MESSAGE("Error: Document is not available for" + filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete || reply->isFinished()) {
|
||||
if (isComplete) {
|
||||
auto cleanedCompletion = removeStopWords(accumulatedResponse);
|
||||
sendCompletionToClient(cleanedCompletion, request, position, true);
|
||||
} else {
|
||||
handleCompletion(request, accumulatedResponse);
|
||||
}
|
||||
m_accumulatedResponses.remove(reply);
|
||||
auto updatedContext = prepareContext(request, documentInfo);
|
||||
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
||||
: 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) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO refactor to dynamic presets system
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
// TODO refactor networking
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} 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.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
config.providerRequest["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (m_completeSettings.useSystemPrompt())
|
||||
systemPrompt.append(
|
||||
m_completeSettings.useUserMessageTemplateForCC()
|
||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||
? m_completeSettings.systemPromptForNonFimModels()
|
||||
: m_completeSettings.systemPrompt());
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for completion");
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContext.fileContext.has_value())
|
||||
systemPrompt.append(updatedContext.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext()) {
|
||||
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,
|
||||
promptTemplate,
|
||||
updatedContext,
|
||||
LLMCore::RequestType::CodeCompletion,
|
||||
false);
|
||||
|
||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||
if (!errors.isEmpty()) {
|
||||
LOG_MESSAGE("Validate errors for fim request:");
|
||||
LOG_MESSAGES(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleCompletion(const QJsonObject &request,
|
||||
const QString &accumulatedCompletion)
|
||||
{
|
||||
auto updatedContext = prepareContext(request, accumulatedCompletion);
|
||||
sendLLMRequest(request, updatedContext);
|
||||
}
|
||||
|
||||
ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
|
||||
const QString &accumulatedCompletion)
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
QJsonObject doc = params["doc"].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) {
|
||||
logMessage("Error: Document is not available for" + filePath.toString());
|
||||
return {"", ""};
|
||||
}
|
||||
|
||||
int cursorPosition = position["character"].toInt();
|
||||
int lineNumber = position["line"].toInt();
|
||||
|
||||
auto textEditor = TextEditor::BaseTextEditor::currentTextEditor();
|
||||
TextEditor::TextEditorWidget *widget = textEditor->editorWidget();
|
||||
|
||||
DocumentContextReader reader(widget->textDocument());
|
||||
|
||||
QString contextBefore = сontextBefore(widget, lineNumber, cursorPosition);
|
||||
QString contextAfter = сontextAfter(widget, lineNumber, cursorPosition);
|
||||
QString instructions = QString("%1%2").arg(settings().useSpecificInstructions()
|
||||
? reader.getSpecificInstructions()
|
||||
: QString(),
|
||||
settings().useFilePathInContext()
|
||||
? reader.getLanguageAndFileInfo()
|
||||
: QString());
|
||||
|
||||
QString updatedContextBefore = contextBefore + accumulatedCompletion;
|
||||
|
||||
return {updatedContextBefore, contextAfter, instructions};
|
||||
Context::DocumentContextReader
|
||||
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
void LLMClientInterface::updateProvider()
|
||||
QString LLMClientInterface::endpoint(
|
||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||
{
|
||||
m_serverUrl = QUrl(QString("%1%2").arg(settings().url(), settings().endPoint()));
|
||||
QString endpoint;
|
||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
||||
: m_generalSettings.ccEndpointMode.stringValue();
|
||||
if (endpointMode == "Auto") {
|
||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint();
|
||||
} else if (endpointMode == "Custom") {
|
||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
} else if (endpointMode == "FIM") {
|
||||
endpoint = provider->completionEndpoint();
|
||||
} else if (endpointMode == "Chat") {
|
||||
endpoint = provider->chatEndpoint();
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
const QJsonObject &request,
|
||||
const QJsonObject &position,
|
||||
bool isComplete)
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
auto filePath = Context::extractFilePathFromRequest(request);
|
||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = request["id"];
|
||||
|
||||
QJsonObject result;
|
||||
QJsonArray completions;
|
||||
QJsonObject completionItem;
|
||||
completionItem[LanguageServerProtocol::textKey] = completion;
|
||||
|
||||
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
|
||||
|
||||
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
|
||||
QString processedCompletion;
|
||||
|
||||
if (outputHandler == "Raw text") {
|
||||
processedCompletion = completion;
|
||||
} else if (outputHandler == "Force processing") {
|
||||
processedCompletion = CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(request));
|
||||
} else { // "Auto"
|
||||
processedCompletion = CodeHandler::hasCodeBlocks(completion)
|
||||
? CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(
|
||||
request))
|
||||
: completion;
|
||||
}
|
||||
|
||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||
QJsonObject range;
|
||||
range["start"] = position;
|
||||
QJsonObject end = position;
|
||||
end["character"] = position["character"].toInt() + completion.length();
|
||||
end["character"] = position["character"].toInt() + processedCompletion.length();
|
||||
range["end"] = end;
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
@ -318,103 +431,17 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
|
||||
result[LanguageServerProtocol::isIncompleteKey] = !isComplete;
|
||||
response[LanguageServerProtocol::resultKey] = result;
|
||||
|
||||
logMessage(
|
||||
LOG_MESSAGE(
|
||||
QString("Completions: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
|
||||
|
||||
logMessage(QString("Full response: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||
LOG_MESSAGE(
|
||||
QString("Full response: \n%1")
|
||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
endTimeMeasurement(requestId);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendLLMRequest(const QJsonObject &request, const ContextData &prompt)
|
||||
{
|
||||
QJsonObject providerRequest = {{"model", settings().modelName.value()}, {"stream", true}};
|
||||
|
||||
auto currentTemplate = PromptTemplateManager::instance().getCurrentTemplate();
|
||||
currentTemplate->prepareRequest(providerRequest, prompt);
|
||||
|
||||
auto &providerManager = LLMProvidersManager::instance();
|
||||
providerManager.getCurrentProvider()->prepareRequest(providerRequest);
|
||||
|
||||
logMessage(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
|
||||
.arg(m_serverUrl.toString(),
|
||||
QString::fromUtf8(
|
||||
QJsonDocument(providerRequest).toJson(QJsonDocument::Indented))));
|
||||
|
||||
QNetworkRequest networkRequest(m_serverUrl);
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
if (providerRequest.contains("api_key")) {
|
||||
QString apiKey = providerRequest["api_key"].toString();
|
||||
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
|
||||
providerRequest.remove("api_key");
|
||||
}
|
||||
|
||||
QNetworkReply *reply = m_manager->post(networkRequest, QJsonDocument(providerRequest).toJson());
|
||||
if (!reply) {
|
||||
logMessage("Error: Failed to create network reply");
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_activeRequests[requestId] = reply;
|
||||
|
||||
connect(reply, &QNetworkReply::readyRead, this, [this, reply, request]() {
|
||||
handleLLMResponse(reply, request);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
|
||||
reply->deleteLater();
|
||||
m_activeRequests.remove(requestId);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
logMessage(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
|
||||
} else {
|
||||
logMessage("Request finished successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QString LLMClientInterface::removeStopWords(const QString &completion)
|
||||
{
|
||||
QString filteredCompletion = completion;
|
||||
|
||||
auto currentTemplate = PromptTemplateManager::instance().getCurrentTemplate();
|
||||
QStringList stopWords = currentTemplate->stopWords();
|
||||
|
||||
for (const QString &stopWord : stopWords) {
|
||||
filteredCompletion = filteredCompletion.replace(stopWord, "");
|
||||
}
|
||||
|
||||
return filteredCompletion;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
logMessage(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
|
||||
}
|
||||
|
||||
void LLMClientInterface::parseCurrentMessage() {}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -22,7 +22,15 @@
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "QodeAssistData.hpp"
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <context/ProgrammingLanguage.hpp>
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkAccessManager;
|
||||
@ -34,29 +42,33 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LLMClientInterface();
|
||||
LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
LLMCore::IProviderRegistry &providerRegistry,
|
||||
LLMCore::IPromptProvider *promptProvider,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger);
|
||||
~LLMClientInterface() override;
|
||||
|
||||
public:
|
||||
Utils::FilePath serverDeviceTemplate() const override;
|
||||
|
||||
void sendCompletionToClient(const QString &completion,
|
||||
const QJsonObject &request,
|
||||
const QJsonObject &position,
|
||||
bool isComplete);
|
||||
void sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete);
|
||||
|
||||
void handleCompletion(const QJsonObject &request,
|
||||
const QString &accumulatedCompletion = QString());
|
||||
void sendLLMRequest(const QJsonObject &request, const ContextData &prompt);
|
||||
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request);
|
||||
void handleCompletion(const QJsonObject &request);
|
||||
|
||||
ContextData prepareContext(const QJsonObject &request,
|
||||
const QString &accumulatedCompletion = QString{});
|
||||
void updateProvider();
|
||||
// exposed for tests
|
||||
void sendData(const QByteArray &data) override;
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
protected:
|
||||
void startImpl() override;
|
||||
void sendData(const QByteArray &data) override;
|
||||
void parseCurrentMessage() override;
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void handleInitialize(const QJsonObject &request);
|
||||
@ -64,26 +76,27 @@ private:
|
||||
void handleTextDocumentDidOpen(const QJsonObject &request);
|
||||
void handleInitialized(const QJsonObject &request);
|
||||
void handleExit(const QJsonObject &request);
|
||||
void handleCancelRequest(const QJsonObject &request);
|
||||
bool processSingleLineCompletion(QNetworkReply *reply,
|
||||
const QJsonObject &request,
|
||||
const QString &accumulatedCompletion);
|
||||
void handleCancelRequest();
|
||||
|
||||
QString сontextBefore(TextEditor::TextEditorWidget *widget, int lineNumber, int cursorPosition);
|
||||
QString сontextAfter(TextEditor::TextEditorWidget *widget, int lineNumber, int cursorPosition);
|
||||
QString removeStopWords(const QString &completion);
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
QUrl m_serverUrl;
|
||||
QNetworkAccessManager *m_manager;
|
||||
QMap<QString, QNetworkReply *> m_activeRequests;
|
||||
QMap<QNetworkReply *, QString> m_accumulatedResponses;
|
||||
LLMCore::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
LLMCore::IProviderRegistry &m_providerRegistry;
|
||||
Context::IDocumentReader &m_documentReader;
|
||||
IRequestPerformanceLogger &m_performanceLogger;
|
||||
QElapsedTimer m_completionTimer;
|
||||
QMap<QString, qint64> m_requestStartTimes;
|
||||
|
||||
void startTimeMeasurement(const QString &requestId);
|
||||
void endTimeMeasurement(const QString &requestId);
|
||||
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
|
||||
Context::ContextManager *m_contextManager;
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* 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
|
||||
@ -18,63 +23,178 @@
|
||||
*/
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/stringutils.h>
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
|
||||
: m_completion(completion)
|
||||
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
|
||||
{
|
||||
int startPos = completion.range().start().toPositionInDocument(origin);
|
||||
int endPos = completion.range().end().toPositionInDocument(origin);
|
||||
if (suggestion.isEmpty() || rightText.isEmpty()) {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
startPos = qBound(0, startPos, origin->characterCount() - 1);
|
||||
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
|
||||
int j = 0;
|
||||
QString processed = rightText;
|
||||
QSet<int> matchedPositions;
|
||||
|
||||
m_start = QTextCursor(origin);
|
||||
m_start.setPosition(startPos);
|
||||
m_start.setKeepPositionOnInsert(true);
|
||||
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
|
||||
if (suggestion[i] == processed[j]) {
|
||||
matchedPositions.insert(j);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
|
||||
QTextCursor cursor(origin);
|
||||
if (matchedPositions.isEmpty()) {
|
||||
return suggestion + rightText;
|
||||
}
|
||||
|
||||
QList<int> positions = matchedPositions.values();
|
||||
std::sort(positions.begin(), positions.end(), std::greater<int>());
|
||||
for (int pos : positions) {
|
||||
processed.remove(pos, 1);
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
LLMSuggestion::LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
|
||||
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
|
||||
{
|
||||
const auto &data = suggestions[currentCompletion];
|
||||
|
||||
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
|
||||
int endPos = data.range.end.toPositionInDocument(sourceDocument);
|
||||
|
||||
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
||||
endPos = qBound(startPos, endPos, sourceDocument->characterCount());
|
||||
|
||||
QTextCursor cursor(sourceDocument);
|
||||
cursor.setPosition(startPos);
|
||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||
|
||||
QTextBlock block = cursor.block();
|
||||
QString blockText = block.text();
|
||||
|
||||
int startPosInBlock = startPos - block.position();
|
||||
int endPosInBlock = endPos - block.position();
|
||||
int cursorPositionInBlock = cursor.positionInBlock();
|
||||
|
||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, completion.text());
|
||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||
|
||||
document()->setPlainText(blockText);
|
||||
|
||||
setCurrentPosition(m_start.position());
|
||||
}
|
||||
|
||||
bool LLMSuggestion::apply()
|
||||
{
|
||||
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
|
||||
cursor.beginEditBlock();
|
||||
cursor.removeSelectedText();
|
||||
cursor.insertText(m_completion.text());
|
||||
cursor.endEditBlock();
|
||||
return true;
|
||||
if (!data.text.contains('\n')) {
|
||||
QString processedRightText = mergeWithRightText(data.text, rightText);
|
||||
processedRightText = processedRightText.mid(data.text.length());
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text
|
||||
+ processedRightText;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
} else {
|
||||
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
|
||||
replacementDocument()->setPlainText(displayText);
|
||||
}
|
||||
}
|
||||
|
||||
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
Q_UNUSED(widget)
|
||||
return apply();
|
||||
return applyPart(Word, widget);
|
||||
}
|
||||
|
||||
void LLMSuggestion::reset()
|
||||
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
m_start.removeSelectedText();
|
||||
return applyPart(Line, widget);
|
||||
}
|
||||
|
||||
int LLMSuggestion::position()
|
||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
return m_start.position();
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
QTextCursor currentCursor = widget->textCursor();
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
|
||||
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
|
||||
+ (cursor.selectionEnd() - cursor.selectionStart());
|
||||
|
||||
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
|
||||
|
||||
if (next == -1) {
|
||||
if (part == Line) {
|
||||
next = text.length();
|
||||
} else {
|
||||
return apply();
|
||||
}
|
||||
}
|
||||
|
||||
if (part == Line)
|
||||
++next;
|
||||
|
||||
QString subText = text.mid(startPos, next - startPos);
|
||||
|
||||
if (subText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!subText.contains('\n')) {
|
||||
currentCursor.insertText(subText);
|
||||
|
||||
const QString remainingText = text.mid(next);
|
||||
if (!remainingText.isEmpty()) {
|
||||
QTextCursor newCursor = widget->textCursor();
|
||||
const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument(
|
||||
newCursor.document(), newCursor.position());
|
||||
const Utils::Text::Position
|
||||
newEnd{newStart.line, newStart.column + int(remainingText.length())};
|
||||
const Utils::Text::Range newRange{newStart, newEnd};
|
||||
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
|
||||
widget->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
||||
}
|
||||
} else {
|
||||
currentCursor.insertText(subText);
|
||||
|
||||
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
|
||||
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
|
||||
if (!newCompletionText.isEmpty()) {
|
||||
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
|
||||
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
|
||||
const Utils::Text::Range newRange{newStart, newEnd};
|
||||
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
|
||||
widget->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LLMSuggestion::apply()
|
||||
{
|
||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
||||
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
|
||||
const QString text = suggestions()[currentSuggestion()].text;
|
||||
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||
|
||||
QTextCursor editCursor = cursor;
|
||||
|
||||
int firstLineEnd = text.indexOf('\n');
|
||||
if (firstLineEnd != -1) {
|
||||
QString firstLine = text.left(firstLineEnd);
|
||||
QString restOfText = text.mid(firstLineEnd);
|
||||
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
|
||||
editCursor.insertText(mergedFirstLine + restOfText);
|
||||
} else {
|
||||
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
editCursor.removeSelectedText();
|
||||
|
||||
QString mergedText = mergeWithRightText(text, textAfterCursor);
|
||||
editCursor.insertText(mergedText);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* 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
|
||||
@ -19,27 +24,22 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <texteditor/textdocumentlayout.h>
|
||||
|
||||
#include "LSPCompletion.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <texteditor/textsuggestion.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class LLMSuggestion final : public TextEditor::TextSuggestion
|
||||
class LLMSuggestion : public TextEditor::CyclicSuggestion
|
||||
{
|
||||
public:
|
||||
LLMSuggestion(const Completion &completion, QTextDocument *origin);
|
||||
enum Part { Word, Line };
|
||||
|
||||
bool apply() final;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) final;
|
||||
void reset() final;
|
||||
int position() final;
|
||||
LLMSuggestion(
|
||||
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
|
||||
|
||||
const Completion &completion() const { return m_completion; }
|
||||
|
||||
private:
|
||||
Completion m_completion;
|
||||
QTextCursor m_start;
|
||||
bool applyWord(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyLine(TextEditor::TextEditorWidget *widget) override;
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
bool apply() override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -43,6 +43,10 @@ public:
|
||||
{
|
||||
return typedValue<LanguageServerProtocol::Position>(LanguageServerProtocol::positionKey);
|
||||
}
|
||||
void setRange(const LanguageServerProtocol::Range &range)
|
||||
{
|
||||
insert(LanguageServerProtocol::rangeKey, range);
|
||||
}
|
||||
LanguageServerProtocol::Range range() const
|
||||
{
|
||||
return typedValue<LanguageServerProtocol::Range>(LanguageServerProtocol::rangeKey);
|
||||
@ -64,9 +68,10 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
|
||||
public:
|
||||
static constexpr LanguageServerProtocol::Key docKey{"doc"};
|
||||
|
||||
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||
int version,
|
||||
const LanguageServerProtocol::Position &position)
|
||||
GetCompletionParams(
|
||||
const LanguageServerProtocol::TextDocumentIdentifier &document,
|
||||
int version,
|
||||
const LanguageServerProtocol::Position &position)
|
||||
{
|
||||
setTextDocument(document);
|
||||
setVersion(version);
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.0.7",
|
||||
"CompatVersion" : "${IDE_VERSION_COMPAT}",
|
||||
"Version" : "0.8.2",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
"License" : "GNU General Public License Usage
|
||||
|
||||
Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.",
|
||||
"Description" : ["QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code",
|
||||
"Prerequisites:",
|
||||
"- One of the supported LLM providers installed (e.g., Ollama or LM Studio)",
|
||||
"- A compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2)"],
|
||||
"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).",
|
||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||
${IDE_PLUGIN_DEPENDENCIES}
|
||||
}
|
||||
|
||||
@ -2,5 +2,13 @@
|
||||
<qresource prefix="/">
|
||||
<file>resources/images/qoderassist-icon@2x.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>
|
||||
</RCC>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of Qode Assist.
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
@ -24,14 +24,20 @@
|
||||
|
||||
#include "QodeAssistClient.hpp"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QTimer>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <languageclient/languageclientsettings.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include "QodeAssistSettings.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProjectSettings.hpp"
|
||||
#include <context/ChangesManager.h>
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace TextEditor;
|
||||
@ -41,16 +47,20 @@ using namespace Core;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QodeAssistClient::QodeAssistClient()
|
||||
: LanguageClient::Client(new LLMClientInterface())
|
||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
: LanguageClient::Client(clientInterface)
|
||||
, m_llmClient(clientInterface)
|
||||
, m_recentCharCount(0)
|
||||
{
|
||||
setName("Qode Assist");
|
||||
setName("QodeAssist");
|
||||
LanguageClient::LanguageFilter filter;
|
||||
filter.mimeTypes = QStringList() << "*";
|
||||
setSupportedLanguage(filter);
|
||||
|
||||
start();
|
||||
setupConnections();
|
||||
|
||||
m_typingTimer.start();
|
||||
}
|
||||
|
||||
QodeAssistClient::~QodeAssistClient()
|
||||
@ -65,29 +75,70 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
return;
|
||||
|
||||
Client::openDocument(document);
|
||||
connect(document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
Q_UNUSED(charsRemoved)
|
||||
if (!settings().enableAutoComplete())
|
||||
return;
|
||||
connect(
|
||||
document,
|
||||
&TextDocument::contentsChangedWithPosition,
|
||||
this,
|
||||
[this, document](int position, int charsRemoved, int charsAdded) {
|
||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||
return;
|
||||
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
auto textEditor = BaseTextEditor::currentTextEditor();
|
||||
if (!textEditor || textEditor->document() != document)
|
||||
return;
|
||||
|
||||
if (Settings::codeCompletionSettings().useProjectChangesCache())
|
||||
Context::ChangesManager::instance()
|
||||
.addChange(document, position, charsRemoved, charsAdded);
|
||||
|
||||
TextEditorWidget *widget = textEditor->editorWidget();
|
||||
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
|
||||
return;
|
||||
|
||||
const int cursorPosition = widget->textCursor().position();
|
||||
if (cursorPosition < position || cursorPosition > position + charsAdded)
|
||||
return;
|
||||
|
||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = widget->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||
QString lastChar = cursor.selectedText();
|
||||
|
||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
m_recentCharCount += charsAdded;
|
||||
|
||||
if (m_typingTimer.elapsed()
|
||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||
m_recentCharCount = charsAdded;
|
||||
m_typingTimer.restart();
|
||||
}
|
||||
|
||||
if (m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
||||
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)
|
||||
@ -102,14 +153,26 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
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;
|
||||
}
|
||||
|
||||
MultiTextCursor cursor = editor->multiTextCursor();
|
||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||
return;
|
||||
|
||||
const FilePath filePath = editor->textDocument()->filePath();
|
||||
GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||
documentVersion(filePath),
|
||||
Position(cursor.mainCursor())}};
|
||||
GetCompletionRequest request{
|
||||
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
|
||||
documentVersion(filePath),
|
||||
Position(cursor.mainCursor())}};
|
||||
if (Settings::codeCompletionSettings().showProgressWidget()) {
|
||||
m_progressHandler.showProgress(editor);
|
||||
}
|
||||
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
|
||||
const GetCompletionRequest::Response &response) {
|
||||
QTC_ASSERT(editor, return);
|
||||
@ -119,6 +182,35 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
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);
|
||||
}
|
||||
|
||||
m_progressHandler.showProgress(editor);
|
||||
m_refactorHandler->sendRefactorRequest(editor, instructions);
|
||||
}
|
||||
|
||||
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
cancelRunningRequest(editor);
|
||||
@ -130,7 +222,9 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
||||
if (editor
|
||||
&& editor->textCursor().position()
|
||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt())
|
||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
||||
&& m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
||||
requestCompletions(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||
@ -144,11 +238,10 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
}
|
||||
|
||||
it.value()->setProperty("cursorPosition", editor->textCursor().position());
|
||||
it.value()->start(settings().startSuggestionTimer());
|
||||
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
|
||||
}
|
||||
|
||||
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
|
||||
TextEditor::TextEditorWidget *editor)
|
||||
void QodeAssistClient::handleCompletions(
|
||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
if (response.error())
|
||||
log(*response.error());
|
||||
@ -168,8 +261,8 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
auto isValidCompletion = [](const Completion &completion) {
|
||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||
};
|
||||
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
|
||||
isValidCompletion);
|
||||
QList<Completion> completions
|
||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||
|
||||
// remove trailing whitespaces from the end of the completions
|
||||
for (Completion &completion : completions) {
|
||||
@ -186,11 +279,19 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
|
||||
if (delta > 0)
|
||||
completion.setText(completionText.chopped(delta));
|
||||
}
|
||||
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
||||
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||
return Text::Position{pos.line() + 1, pos.character()};
|
||||
};
|
||||
|
||||
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||
Text::Position pos{toTextPos(c.position())};
|
||||
return TextSuggestion::Data{range, pos, c.text()};
|
||||
});
|
||||
m_progressHandler.hideProgress();
|
||||
if (completions.isEmpty())
|
||||
return;
|
||||
editor->insertSuggestion(
|
||||
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
|
||||
editor->addHoverHandler(&m_hoverHandler);
|
||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,13 +300,18 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
||||
const auto it = m_runningRequests.constFind(editor);
|
||||
if (it == m_runningRequests.constEnd())
|
||||
return;
|
||||
m_progressHandler.hideProgress();
|
||||
cancelRequest(it->id());
|
||||
m_runningRequests.erase(it);
|
||||
}
|
||||
|
||||
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
|
||||
{
|
||||
return settings().enableQodeAssist();
|
||||
if (!project)
|
||||
return Settings::generalSettings().enableQodeAssist();
|
||||
|
||||
Settings::ProjectSettings settings(project);
|
||||
return settings.isEnabled();
|
||||
}
|
||||
|
||||
void QodeAssistClient::setupConnections()
|
||||
@ -215,18 +321,13 @@ void QodeAssistClient::setupConnections()
|
||||
openDocument(textDocument);
|
||||
};
|
||||
|
||||
m_documentOpenedConnection = connect(EditorManager::instance(),
|
||||
&EditorManager::documentOpened,
|
||||
this,
|
||||
openDoc);
|
||||
m_documentClosedConnection = connect(EditorManager::instance(),
|
||||
&EditorManager::documentClosed,
|
||||
this,
|
||||
[this](IDocument *document) {
|
||||
if (auto textDocument = qobject_cast<TextDocument *>(
|
||||
document))
|
||||
closeDocument(textDocument);
|
||||
});
|
||||
m_documentOpenedConnection
|
||||
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
|
||||
m_documentClosedConnection = connect(
|
||||
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
|
||||
if (auto textDocument = qobject_cast<TextDocument *>(document))
|
||||
closeDocument(textDocument);
|
||||
});
|
||||
|
||||
for (IDocument *doc : DocumentModel::openedDocuments())
|
||||
openDoc(doc);
|
||||
@ -237,13 +338,36 @@ void QodeAssistClient::cleanupConnections()
|
||||
disconnect(m_documentOpenedConnection);
|
||||
disconnect(m_documentClosedConnection);
|
||||
|
||||
for (IEditor *editor : DocumentModel::editorsForOpenedDocuments()) {
|
||||
if (auto textEditor = qobject_cast<BaseTextEditor *>(editor))
|
||||
textEditor->editorWidget()->removeHoverHandler(&m_hoverHandler);
|
||||
}
|
||||
|
||||
qDeleteAll(m_scheduledRequests);
|
||||
m_scheduledRequests.clear();
|
||||
}
|
||||
|
||||
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||
{
|
||||
if (!result.success) {
|
||||
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
auto editor = BaseTextEditor::currentTextEditor();
|
||||
if (!editor) {
|
||||
LOG_MESSAGE("Refactoring failed: No active editor found");
|
||||
return;
|
||||
}
|
||||
|
||||
auto editorWidget = editor->editorWidget();
|
||||
|
||||
QTextCursor cursor = editorWidget->textCursor();
|
||||
cursor.beginEditBlock();
|
||||
|
||||
int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document());
|
||||
int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document());
|
||||
|
||||
cursor.setPosition(startPos);
|
||||
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||
|
||||
cursor.insertText(result.newText);
|
||||
cursor.endEditBlock();
|
||||
m_progressHandler.hideProgress();
|
||||
}
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of Qode Assist.
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
@ -24,39 +24,55 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <languageclient/client.h>
|
||||
#include <QObject>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LSPCompletion.hpp"
|
||||
#include "QodeAssistHoverHandler.hpp"
|
||||
#include "QuickRefactorHandler.hpp"
|
||||
#include "widgets/CompletionProgressHandler.hpp"
|
||||
#include "widgets/EditorChatButtonHandler.hpp"
|
||||
#include <languageclient/client.h>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class QodeAssistClient : public LanguageClient::Client
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit QodeAssistClient();
|
||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||
~QodeAssistClient() override;
|
||||
|
||||
void openDocument(TextEditor::TextDocument *document) override;
|
||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||
|
||||
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
||||
void requestQuickRefactor(
|
||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||
|
||||
private:
|
||||
void scheduleRequest(TextEditor::TextEditorWidget *editor);
|
||||
void handleCompletions(const GetCompletionRequest::Response &response,
|
||||
TextEditor::TextEditorWidget *editor);
|
||||
void handleCompletions(
|
||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
|
||||
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
||||
bool isEnabled(ProjectExplorer::Project *project) const;
|
||||
|
||||
void setupConnections();
|
||||
void cleanupConnections();
|
||||
void handleRefactoringResult(const RefactorResult &result);
|
||||
|
||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||
QodeAssistHoverHandler m_hoverHandler;
|
||||
QMetaObject::Connection m_documentOpenedConnection;
|
||||
QMetaObject::Connection m_documentClosedConnection;
|
||||
|
||||
QElapsedTimer m_typingTimer;
|
||||
int m_recentCharCount;
|
||||
CompletionProgressHandler m_progressHandler;
|
||||
EditorChatButtonHandler m_chatButtonHandler;
|
||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||
LLMClientInterface *m_llmClient;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@ -24,43 +24,6 @@ namespace QodeAssist::Constants {
|
||||
const char ACTION_ID[] = "QodeAssist.Action";
|
||||
const char MENU_ID[] = "QodeAssist.Menu";
|
||||
|
||||
// settings
|
||||
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
|
||||
const char ENABLE_AUTO_COMPLETE[] = "QodeAssist.enableAutoComplete";
|
||||
const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
|
||||
const char LLM_PROVIDERS[] = "QodeAssist.llmProviders";
|
||||
const char URL[] = "QodeAssist.url";
|
||||
const char END_POINT[] = "QodeAssist.endPoint";
|
||||
const char MODEL_NAME[] = "QodeAssist.modelName";
|
||||
const char SELECT_MODELS[] = "QodeAssist.selectModels";
|
||||
const char FIM_PROMPTS[] = "QodeAssist.fimPrompts";
|
||||
const char TEMPERATURE[] = "QodeAssist.temperature";
|
||||
const char MAX_TOKENS[] = "QodeAssist.maxTokens";
|
||||
const char READ_FULL_FILE[] = "QodeAssist.readFullFile";
|
||||
const char READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.readStringsBeforeCursor";
|
||||
const char READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.readStringsAfterCursor";
|
||||
const char USE_TOP_P[] = "QodeAssist.useTopP";
|
||||
const char TOP_P[] = "QodeAssist.topP";
|
||||
const char USE_TOP_K[] = "QodeAssist.useTopK";
|
||||
const char TOP_K[] = "QodeAssist.topK";
|
||||
const char USE_PRESENCE_PENALTY[] = "QodeAssist.usePresencePenalty";
|
||||
const char PRESENCE_PENALTY[] = "QodeAssist.presencePenalty";
|
||||
const char USE_FREQUENCY_PENALTY[] = "QodeAssist.useFrequencyPenalty";
|
||||
const char FREQUENCY_PENALTY[] = "QodeAssist.frequencyPenalty";
|
||||
const char PROVIDER_PATHS[] = "QodeAssist.providerPaths";
|
||||
const char START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
|
||||
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
|
||||
const char OLLAMA_LIVETIME[] = "QodeAssist.ollamaLivetime";
|
||||
const char SPECIFIC_INSTRUCTIONS[] = "QodeAssist.specificInstractions";
|
||||
const char MULTILINE_COMPLETION[] = "QodeAssist.multilineCompletion";
|
||||
const char API_KEY[] = "QodeAssist.apiKey";
|
||||
const char USE_SPECIFIC_INSTRUCTIONS[] = "QodeAssist.useSpecificInstructions";
|
||||
const char USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.useFilePathInContext";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "Qode Assist";
|
||||
|
||||
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
*
|
||||
* This file is part of Qode Assist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* 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 "QodeAssistHoverHandler.hpp"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QScopeGuard>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/textdocumentlayout.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <utils/tooltip/tooltip.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include "LSPCompletion.hpp"
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
using namespace TextEditor;
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace Utils;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class QodeAssistCompletionToolTip : public QToolBar
|
||||
{
|
||||
public:
|
||||
QodeAssistCompletionToolTip(TextEditorWidget *editor)
|
||||
: m_editor(editor)
|
||||
{
|
||||
auto apply = addAction(Tr::tr("Apply (%1)").arg(QKeySequence(Qt::Key_Tab).toString()));
|
||||
connect(apply, &QAction::triggered, this, &QodeAssistCompletionToolTip::apply);
|
||||
}
|
||||
|
||||
private:
|
||||
void apply()
|
||||
{
|
||||
if (TextSuggestion *suggestion = m_editor->currentSuggestion()) {
|
||||
if (!suggestion->apply())
|
||||
return;
|
||||
}
|
||||
ToolTip::hide();
|
||||
}
|
||||
|
||||
TextEditorWidget *m_editor;
|
||||
};
|
||||
|
||||
void QodeAssistHoverHandler::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();
|
||||
auto *suggestion = dynamic_cast<LLMSuggestion *>(TextDocumentLayout::suggestion(m_block));
|
||||
|
||||
if (!suggestion)
|
||||
return;
|
||||
|
||||
const Completion completion = suggestion->completion();
|
||||
if (completion.text().isEmpty())
|
||||
return;
|
||||
|
||||
cleanup.dismiss();
|
||||
report(Priority_Suggestion);
|
||||
}
|
||||
|
||||
void QodeAssistHoverHandler::operateTooltip(TextEditor::TextEditorWidget *editorWidget,
|
||||
const QPoint &point)
|
||||
{
|
||||
Q_UNUSED(point)
|
||||
auto *suggestion = dynamic_cast<LLMSuggestion *>(TextDocumentLayout::suggestion(m_block));
|
||||
|
||||
if (!suggestion)
|
||||
return;
|
||||
|
||||
auto tooltipWidget = new QodeAssistCompletionToolTip(editorWidget);
|
||||
|
||||
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
|
||||
QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft())
|
||||
- Utils::ToolTip::offsetFromPosition();
|
||||
pos.ry() -= tooltipWidget->sizeHint().height();
|
||||
ToolTip::show(pos, tooltipWidget, editorWidget);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,405 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "QodeAssistSettings.hpp"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QtWidgets/qmessagebox.h>
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
#include "QodeAssisttr.h"
|
||||
|
||||
#include "LLMProvidersManager.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "QodeAssistUtils.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QodeAssistSettings &settings()
|
||||
{
|
||||
static QodeAssistSettings settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
QodeAssistSettings::QodeAssistSettings()
|
||||
{
|
||||
setAutoApply(false);
|
||||
|
||||
enableQodeAssist.setSettingsKey(Constants::ENABLE_QODE_ASSIST);
|
||||
enableQodeAssist.setLabelText(Tr::tr("Enable Qode Assist"));
|
||||
enableQodeAssist.setDefaultValue(true);
|
||||
|
||||
enableAutoComplete.setSettingsKey(Constants::ENABLE_AUTO_COMPLETE);
|
||||
enableAutoComplete.setLabelText(Tr::tr("Enable Auto Complete"));
|
||||
enableAutoComplete.setDefaultValue(true);
|
||||
|
||||
enableLogging.setSettingsKey(Constants::ENABLE_LOGGING);
|
||||
enableLogging.setLabelText(Tr::tr("Enable Logging"));
|
||||
enableLogging.setDefaultValue(false);
|
||||
|
||||
llmProviders.setSettingsKey(Constants::LLM_PROVIDERS);
|
||||
llmProviders.setDisplayName(Tr::tr("LLM Providers:"));
|
||||
llmProviders.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
llmProviders.setDefaultValue(1);
|
||||
|
||||
url.setSettingsKey(Constants::URL);
|
||||
url.setLabelText(Tr::tr("URL:"));
|
||||
url.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
endPoint.setSettingsKey(Constants::END_POINT);
|
||||
endPoint.setLabelText(Tr::tr("Endpoint:"));
|
||||
endPoint.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
modelName.setSettingsKey(Constants::MODEL_NAME);
|
||||
modelName.setLabelText(Tr::tr("LLM Name:"));
|
||||
modelName.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
temperature.setSettingsKey(Constants::TEMPERATURE);
|
||||
temperature.setLabelText(Tr::tr("Temperature:"));
|
||||
temperature.setDefaultValue(0.2);
|
||||
temperature.setRange(0.0, 10.0);
|
||||
|
||||
selectModels.m_buttonText = Tr::tr("Select Models");
|
||||
|
||||
ollamaLivetime.setSettingsKey(Constants::OLLAMA_LIVETIME);
|
||||
ollamaLivetime.setLabelText(
|
||||
Tr::tr("Time to suspend Ollama after completion request (in minutes), "
|
||||
"Only Ollama, -1 to disable"));
|
||||
ollamaLivetime.setDefaultValue("5m");
|
||||
ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
|
||||
fimPrompts.setDisplayName(Tr::tr("Fill-In-Middle Prompt"));
|
||||
fimPrompts.setSettingsKey(Constants::FIM_PROMPTS);
|
||||
fimPrompts.setDefaultValue(1);
|
||||
fimPrompts.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
|
||||
readFullFile.setSettingsKey(Constants::READ_FULL_FILE);
|
||||
readFullFile.setLabelText(Tr::tr("Read Full File"));
|
||||
readFullFile.setDefaultValue(false);
|
||||
|
||||
maxFileThreshold.setSettingsKey(Constants::MAX_FILE_THRESHOLD);
|
||||
maxFileThreshold.setLabelText(Tr::tr("Max File Threshold:"));
|
||||
maxFileThreshold.setRange(10, 100000);
|
||||
maxFileThreshold.setDefaultValue(600);
|
||||
|
||||
readStringsBeforeCursor.setSettingsKey(Constants::READ_STRINGS_BEFORE_CURSOR);
|
||||
readStringsBeforeCursor.setLabelText(Tr::tr("Read Strings Before Cursor"));
|
||||
readStringsBeforeCursor.setDefaultValue(50);
|
||||
|
||||
readStringsAfterCursor.setSettingsKey(Constants::READ_STRINGS_AFTER_CURSOR);
|
||||
readStringsAfterCursor.setLabelText(Tr::tr("Read Strings After Cursor"));
|
||||
readStringsAfterCursor.setDefaultValue(30);
|
||||
|
||||
maxTokens.setSettingsKey(Constants::MAX_TOKENS);
|
||||
maxTokens.setLabelText(Tr::tr("Max Tokens"));
|
||||
maxTokens.setRange(-1, 10000);
|
||||
maxTokens.setDefaultValue(250);
|
||||
|
||||
useTopP.setSettingsKey(Constants::USE_TOP_P);
|
||||
useTopP.setDefaultValue(false);
|
||||
|
||||
topP.setSettingsKey(Constants::TOP_P);
|
||||
topP.setLabelText(Tr::tr("top_p"));
|
||||
topP.setDefaultValue(0.9);
|
||||
topP.setRange(0.0, 1.0);
|
||||
|
||||
useTopK.setSettingsKey(Constants::USE_TOP_K);
|
||||
useTopK.setDefaultValue(false);
|
||||
|
||||
topK.setSettingsKey(Constants::TOP_K);
|
||||
topK.setLabelText(Tr::tr("top_k"));
|
||||
topK.setDefaultValue(50);
|
||||
topK.setRange(1, 1000);
|
||||
|
||||
usePresencePenalty.setSettingsKey(Constants::USE_PRESENCE_PENALTY);
|
||||
usePresencePenalty.setDefaultValue(false);
|
||||
|
||||
presencePenalty.setSettingsKey(Constants::PRESENCE_PENALTY);
|
||||
presencePenalty.setLabelText(Tr::tr("presence_penalty"));
|
||||
presencePenalty.setDefaultValue(0.0);
|
||||
presencePenalty.setRange(-2.0, 2.0);
|
||||
|
||||
useFrequencyPenalty.setSettingsKey(Constants::USE_FREQUENCY_PENALTY);
|
||||
useFrequencyPenalty.setDefaultValue(false);
|
||||
|
||||
frequencyPenalty.setSettingsKey(Constants::FREQUENCY_PENALTY);
|
||||
frequencyPenalty.setLabelText(Tr::tr("frequency_penalty"));
|
||||
frequencyPenalty.setDefaultValue(0.0);
|
||||
frequencyPenalty.setRange(-2.0, 2.0);
|
||||
|
||||
providerPaths.setSettingsKey(Constants::PROVIDER_PATHS);
|
||||
providerPaths.setLabelText(Tr::tr("Provider Paths:"));
|
||||
|
||||
startSuggestionTimer.setSettingsKey(Constants::START_SUGGESTION_TIMER);
|
||||
startSuggestionTimer.setLabelText(Tr::tr("Start Suggestion Timer:"));
|
||||
startSuggestionTimer.setRange(10, 10000);
|
||||
startSuggestionTimer.setDefaultValue(500);
|
||||
|
||||
useFilePathInContext.setSettingsKey(Constants::USE_FILE_PATH_IN_CONTEXT);
|
||||
useFilePathInContext.setDefaultValue(false);
|
||||
useFilePathInContext.setLabelText(Tr::tr("Use File Path in Context"));
|
||||
|
||||
useSpecificInstructions.setSettingsKey(Constants::USE_SPECIFIC_INSTRUCTIONS);
|
||||
useSpecificInstructions.setDefaultValue(false);
|
||||
useSpecificInstructions.setLabelText(Tr::tr("Use Specific Instructions"));
|
||||
|
||||
specificInstractions.setSettingsKey(Constants::SPECIFIC_INSTRUCTIONS);
|
||||
specificInstractions.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
|
||||
specificInstractions.setLabelText(
|
||||
Tr::tr("Instructions: Please keep %1 for languge name, warning, it shouldn't too big"));
|
||||
specificInstractions.setDefaultValue(
|
||||
"You are an expert %1 code completion AI."
|
||||
"CRITICAL: Please provide minimal the best possible code completion suggestions.\n");
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset to Defaults");
|
||||
multiLineCompletion.setSettingsKey(Constants::MULTILINE_COMPLETION);
|
||||
multiLineCompletion.setDefaultValue(true);
|
||||
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion"));
|
||||
|
||||
apiKey.setSettingsKey(Constants::API_KEY);
|
||||
apiKey.setLabelText(Tr::tr("API Key:"));
|
||||
apiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
apiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||
|
||||
const auto &manager = LLMProvidersManager::instance();
|
||||
if (!manager.getProviderNames().isEmpty()) {
|
||||
const auto providerNames = manager.getProviderNames();
|
||||
for (const QString &name : providerNames) {
|
||||
llmProviders.addOption(name);
|
||||
}
|
||||
}
|
||||
|
||||
const auto &promptManager = PromptTemplateManager::instance();
|
||||
if (!promptManager.getTemplateNames().isEmpty()) {
|
||||
const auto promptNames = promptManager.getTemplateNames();
|
||||
for (const QString &name : promptNames) {
|
||||
fimPrompts.addOption(name);
|
||||
}
|
||||
}
|
||||
|
||||
readSettings();
|
||||
|
||||
topK.setEnabled(useTopK());
|
||||
topP.setEnabled(useTopP());
|
||||
presencePenalty.setEnabled(usePresencePenalty());
|
||||
frequencyPenalty.setEnabled(useFrequencyPenalty());
|
||||
readStringsAfterCursor.setEnabled(!readFullFile());
|
||||
readStringsBeforeCursor.setEnabled(!readFullFile());
|
||||
specificInstractions.setEnabled(useSpecificInstructions());
|
||||
PromptTemplateManager::instance().setCurrentTemplate(fimPrompts.stringValue());
|
||||
LLMProvidersManager::instance().setCurrentProvider(llmProviders.stringValue());
|
||||
|
||||
setLoggingEnabled(enableLogging());
|
||||
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
return Column{Group{title(Tr::tr("General Settings")),
|
||||
Form{Column{enableQodeAssist,
|
||||
enableAutoComplete,
|
||||
multiLineCompletion,
|
||||
enableLogging,
|
||||
Row{Stretch{1}, resetToDefaults}}}},
|
||||
Group{title(Tr::tr("LLM Providers")),
|
||||
Form{Column{llmProviders, Row{url, endPoint}, providerPaths}}},
|
||||
Group{title(Tr::tr("LLM Model Settings")),
|
||||
Form{Column{Row{selectModels, modelName}}}},
|
||||
Group{title(Tr::tr("FIM Prompt Settings")),
|
||||
Form{Column{fimPrompts,
|
||||
readFullFile,
|
||||
maxFileThreshold,
|
||||
readStringsBeforeCursor,
|
||||
readStringsAfterCursor,
|
||||
ollamaLivetime,
|
||||
apiKey,
|
||||
useFilePathInContext,
|
||||
useSpecificInstructions,
|
||||
specificInstractions,
|
||||
temperature,
|
||||
maxTokens,
|
||||
startSuggestionTimer,
|
||||
Row{useTopP, topP, Stretch{1}},
|
||||
Row{useTopK, topK, Stretch{1}},
|
||||
Row{usePresencePenalty, presencePenalty, Stretch{1}},
|
||||
Row{useFrequencyPenalty, frequencyPenalty, Stretch{1}}}}},
|
||||
st};
|
||||
});
|
||||
|
||||
setupConnections();
|
||||
}
|
||||
|
||||
void QodeAssistSettings::setupConnections()
|
||||
{
|
||||
connect(&llmProviders, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
|
||||
int index = llmProviders.volatileValue();
|
||||
logMessage(QString("currentProvider %1").arg(llmProviders.displayForIndex(index)));
|
||||
LLMProvidersManager::instance().setCurrentProvider(llmProviders.displayForIndex(index));
|
||||
updateProviderSettings();
|
||||
});
|
||||
|
||||
connect(&fimPrompts, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
|
||||
int index = fimPrompts.volatileValue();
|
||||
logMessage(QString("currentPrompt %1").arg(fimPrompts.displayForIndex(index)));
|
||||
PromptTemplateManager::instance().setCurrentTemplate(fimPrompts.displayForIndex(index));
|
||||
});
|
||||
|
||||
connect(&selectModels, &ButtonAspect::clicked, this, [this]() { showModelSelectionDialog(); });
|
||||
connect(&useTopP, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
topP.setEnabled(useTopP.volatileValue());
|
||||
});
|
||||
connect(&useTopK, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
topK.setEnabled(useTopK.volatileValue());
|
||||
});
|
||||
connect(&usePresencePenalty, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
presencePenalty.setEnabled(usePresencePenalty.volatileValue());
|
||||
});
|
||||
connect(&useFrequencyPenalty, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
frequencyPenalty.setEnabled(useFrequencyPenalty.volatileValue());
|
||||
});
|
||||
connect(&readFullFile, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
readStringsAfterCursor.setEnabled(!readFullFile.volatileValue());
|
||||
readStringsBeforeCursor.setEnabled(!readFullFile.volatileValue());
|
||||
});
|
||||
connect(&resetToDefaults,
|
||||
&ButtonAspect::clicked,
|
||||
this,
|
||||
&QodeAssistSettings::resetSettingsToDefaults);
|
||||
connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
setLoggingEnabled(enableLogging.volatileValue());
|
||||
});
|
||||
connect(&useSpecificInstructions, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
|
||||
specificInstractions.setEnabled(useSpecificInstructions.volatileValue());
|
||||
});
|
||||
}
|
||||
|
||||
void QodeAssistSettings::updateProviderSettings()
|
||||
{
|
||||
auto *provider = LLMProvidersManager::instance().getCurrentProvider();
|
||||
|
||||
if (provider) {
|
||||
logMessage(QString("currentProvider %1").arg(provider->name()));
|
||||
url.setValue(provider->url());
|
||||
endPoint.setValue(provider->completionEndpoint());
|
||||
ollamaLivetime.setEnabled(provider->name() == "Ollama");
|
||||
}
|
||||
}
|
||||
|
||||
QStringList QodeAssistSettings::getInstalledModels()
|
||||
{
|
||||
auto *provider = LLMProvidersManager::instance().getCurrentProvider();
|
||||
if (provider) {
|
||||
auto env = getEnvironmentWithProviderPaths();
|
||||
return provider->getInstalledModels(env);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void QodeAssistSettings::showModelSelectionDialog()
|
||||
{
|
||||
QStringList models = getInstalledModels();
|
||||
|
||||
bool ok;
|
||||
QString selectedModel = QInputDialog::getItem(Core::ICore::dialogParent(),
|
||||
Tr::tr("Select LLM Model"),
|
||||
Tr::tr("Choose a model:"),
|
||||
models,
|
||||
0,
|
||||
false,
|
||||
&ok);
|
||||
|
||||
if (ok && !selectedModel.isEmpty()) {
|
||||
modelName.setValue(selectedModel);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Utils::Environment QodeAssistSettings::getEnvironmentWithProviderPaths() const
|
||||
{
|
||||
Utils::Environment env = Utils::Environment::systemEnvironment();
|
||||
const QStringList additionalPaths = providerPaths.volatileValue();
|
||||
for (const QString &path : additionalPaths) {
|
||||
env.prependOrSetPath(path);
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
void QodeAssistSettings::resetSettingsToDefaults()
|
||||
{
|
||||
QMessageBox::StandardButton reply;
|
||||
reply = QMessageBox::question(
|
||||
Core::ICore::dialogParent(),
|
||||
Tr::tr("Reset Settings"),
|
||||
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(enableQodeAssist);
|
||||
resetAspect(enableAutoComplete);
|
||||
resetAspect(llmProviders);
|
||||
resetAspect(url);
|
||||
resetAspect(endPoint);
|
||||
resetAspect(modelName);
|
||||
resetAspect(fimPrompts);
|
||||
resetAspect(temperature);
|
||||
resetAspect(maxTokens);
|
||||
resetAspect(readFullFile);
|
||||
resetAspect(maxFileThreshold);
|
||||
resetAspect(readStringsBeforeCursor);
|
||||
resetAspect(readStringsAfterCursor);
|
||||
resetAspect(useTopP);
|
||||
resetAspect(topP);
|
||||
resetAspect(useTopK);
|
||||
resetAspect(topK);
|
||||
resetAspect(usePresencePenalty);
|
||||
resetAspect(presencePenalty);
|
||||
resetAspect(useFrequencyPenalty);
|
||||
resetAspect(frequencyPenalty);
|
||||
resetAspect(startSuggestionTimer);
|
||||
resetAspect(enableLogging);
|
||||
resetAspect(ollamaLivetime);
|
||||
resetAspect(specificInstractions);
|
||||
|
||||
updateProviderSettings();
|
||||
apply();
|
||||
|
||||
QMessageBox::information(Core::ICore::dialogParent(),
|
||||
Tr::tr("Settings Reset"),
|
||||
Tr::tr("All settings have been reset to their default values."));
|
||||
}
|
||||
}
|
||||
|
||||
class QodeAssistSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
QodeAssistSettingsPage()
|
||||
{
|
||||
setId(Constants::QODE_ASSIST_GENERAL_OPTIONS_ID);
|
||||
setDisplayName("Qode Assist");
|
||||
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||
setDisplayCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY);
|
||||
setCategoryIconPath(":/resources/images/qoderassist-icon.png");
|
||||
setSettingsProvider([] { return &settings(); });
|
||||
}
|
||||
};
|
||||
|
||||
const QodeAssistSettingsPage settingsPage;
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <QPushButton>
|
||||
#include <utils/aspects.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
template<typename AspectType>
|
||||
void resetAspect(AspectType &aspect)
|
||||
{
|
||||
aspect.setValue(aspect.defaultValue());
|
||||
}
|
||||
|
||||
class ButtonAspect : public Utils::BaseAspect
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ButtonAspect(Utils::AspectContainer *container = nullptr)
|
||||
: Utils::BaseAspect(container)
|
||||
{}
|
||||
|
||||
void addToLayout(Layouting::Layout &parent) override
|
||||
{
|
||||
auto button = new QPushButton(m_buttonText);
|
||||
connect(button, &QPushButton::clicked, this, &ButtonAspect::clicked);
|
||||
parent.addItem(button);
|
||||
}
|
||||
|
||||
QString m_buttonText;
|
||||
signals:
|
||||
void clicked();
|
||||
};
|
||||
|
||||
class QodeAssistSettings : public Utils::AspectContainer
|
||||
{
|
||||
public:
|
||||
QodeAssistSettings();
|
||||
|
||||
Utils::BoolAspect enableQodeAssist{this};
|
||||
Utils::BoolAspect enableAutoComplete{this};
|
||||
Utils::BoolAspect enableLogging{this};
|
||||
|
||||
Utils::SelectionAspect llmProviders{this};
|
||||
Utils::StringAspect url{this};
|
||||
Utils::StringAspect endPoint{this};
|
||||
|
||||
Utils::StringAspect modelName{this};
|
||||
ButtonAspect selectModels{this};
|
||||
|
||||
Utils::SelectionAspect fimPrompts{this};
|
||||
Utils::DoubleAspect temperature{this};
|
||||
Utils::IntegerAspect maxTokens{this};
|
||||
|
||||
Utils::BoolAspect readFullFile{this};
|
||||
Utils::IntegerAspect readStringsBeforeCursor{this};
|
||||
Utils::IntegerAspect readStringsAfterCursor{this};
|
||||
|
||||
Utils::BoolAspect useTopP{this};
|
||||
Utils::DoubleAspect topP{this};
|
||||
|
||||
Utils::BoolAspect useTopK{this};
|
||||
Utils::IntegerAspect topK{this};
|
||||
|
||||
Utils::BoolAspect usePresencePenalty{this};
|
||||
Utils::DoubleAspect presencePenalty{this};
|
||||
|
||||
Utils::BoolAspect useFrequencyPenalty{this};
|
||||
Utils::DoubleAspect frequencyPenalty{this};
|
||||
|
||||
Utils::StringListAspect providerPaths{this};
|
||||
|
||||
Utils::IntegerAspect startSuggestionTimer{this};
|
||||
Utils::IntegerAspect maxFileThreshold{this};
|
||||
|
||||
Utils::StringAspect ollamaLivetime{this};
|
||||
Utils::StringAspect specificInstractions{this};
|
||||
Utils::BoolAspect useSpecificInstructions{this};
|
||||
Utils::BoolAspect useFilePathInContext{this};
|
||||
Utils::BoolAspect multiLineCompletion{this};
|
||||
|
||||
Utils::StringAspect apiKey{this};
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void updateProviderSettings();
|
||||
QStringList getInstalledModels();
|
||||
void showModelSelectionDialog();
|
||||
Utils::Environment getEnvironmentWithProviderPaths() const;
|
||||
void resetSettingsToDefaults();
|
||||
};
|
||||
|
||||
QodeAssistSettings &settings();
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 <QString>
|
||||
#include <coreplugin/messagemanager.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
inline bool &loggingEnabled()
|
||||
{
|
||||
static bool enabled = false;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
inline void setLoggingEnabled(bool enable)
|
||||
{
|
||||
loggingEnabled() = enable;
|
||||
}
|
||||
|
||||
inline void logMessage(const QString &message, bool silent = true)
|
||||
{
|
||||
if (!loggingEnabled())
|
||||
return;
|
||||
|
||||
const QString prefixedMessage = QLatin1String("[Qode Assist] ") + message;
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessage);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
inline void logMessages(const QStringList &messages, bool silent = true)
|
||||
{
|
||||
if (!loggingEnabled())
|
||||
return;
|
||||
|
||||
QStringList prefixedMessages;
|
||||
for (const QString &message : messages) {
|
||||
prefixedMessages << (QLatin1String("[Qode Assist] ") + message);
|
||||
}
|
||||
if (silent) {
|
||||
Core::MessageManager::writeSilently(prefixedMessages);
|
||||
} else {
|
||||
Core::MessageManager::writeFlashing(prefixedMessages);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="en_001"></TS>
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Petr Mironychev
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
|
||||
335
QuickRefactorHandler.cpp
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* 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>
|
||||
|
||||
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.caProvider();
|
||||
auto provider = providerRegistry.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = QString("No provider found with name: %1").arg(providerName);
|
||||
emit refactoringCompleted(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto templateName = settings.caTemplate();
|
||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = QString("No template found with name: %1").arg(templateName);
|
||||
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.caUrl(), provider->chatEndpoint());
|
||||
config.providerRequest = {{"model", settings.caModel()}, {"stream", true}};
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
LLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
|
||||
provider->prepareRequest(
|
||||
config.providerRequest,
|
||||
promptTemplate,
|
||||
context,
|
||||
LLMCore::RequestType::QuickRefactoring,
|
||||
false);
|
||||
|
||||
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();
|
||||
|
||||
// TODO add selecting content before and after cursor/selection
|
||||
QString fullContent = documentInfo.document->toPlainText();
|
||||
QString taggedContent = fullContent;
|
||||
|
||||
if (cursor.hasSelection()) {
|
||||
int selEnd = cursor.selectionEnd();
|
||||
int selStart = cursor.selectionStart();
|
||||
taggedContent
|
||||
.insert(selEnd, selEnd == cursorPos ? "<selection_end><cursor>" : "<selection_end>");
|
||||
taggedContent.insert(
|
||||
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
|
||||
} else {
|
||||
taggedContent.insert(cursorPos, "<cursor>");
|
||||
}
|
||||
|
||||
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
|
||||
|
||||
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) {
|
||||
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;
|
||||
|
||||
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) {
|
||||
QJsonObject request{{"id", requestId}};
|
||||
handleLLMResponse(fullText, request, true);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
88
QuickRefactorHandler.hpp
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
|
||||
class QuickRefactorHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
||||
~QuickRefactorHandler() override;
|
||||
|
||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||
|
||||
void cancelRequest();
|
||||
|
||||
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
|
||||
294
README.md
@ -1,62 +1,223 @@
|
||||
# QodeAssist
|
||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||

|
||||

|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
|
||||
QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
|
||||
 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.
|
||||
|
||||
## Supported LLM Providers
|
||||
QodeAssist currently supports the following LLM (Large Language Model) providers:
|
||||
- [Ollama](https://ollama.com)
|
||||
- [LM Studio](https://lmstudio.ai)
|
||||
- OpenAI compatible providers
|
||||
⚠️ **Important Notice About Paid Providers**
|
||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||
> - These services will consume API tokens which may result in charges to your account
|
||||
> - The QodeAssist developer bears no responsibility for any charges incurred
|
||||
> - Please carefully review the provider's pricing and your account settings before use
|
||||
|
||||
## Supported Models
|
||||
QodeAssist has been tested with the following language models, all trained for Fill-in-theMiddle:
|
||||
## Table of Contents
|
||||
1. [Overview](#overview)
|
||||
2. [Install Plugin](#install-plugin-to-qtcreator)
|
||||
3. [Configuration](#configuration)
|
||||
4. [Features](#features)
|
||||
5. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||
6. [Hotkeys](#hotkeys)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
8. [Development Progress](#development-progress)
|
||||
9. [Support the Development](#support-the-development-of-qodeassist)
|
||||
10. [How to Build](#how-to-build)
|
||||
|
||||
Ollama:
|
||||
- [starcoder2](https://ollama.com/library/starcoder2)
|
||||
- [codellama](https://ollama.com/library/codellama)
|
||||
## Overview
|
||||
|
||||
LM studio:
|
||||
- [second-state/StarCoder2-7B-GGUF](https://huggingface.co/second-state/StarCoder2-7B-GGUF)
|
||||
- [TheBloke/CodeLlama-7B-GGUF](https://huggingface.co/TheBloke/CodeLlama-7B-GGUF)
|
||||
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
||||
|
||||
Please note that while these models have been specifically tested and confirmed to work well with QodeAssist, other models compatible with the supported providers may also work. We encourage users to experiment with different models and report their experiences.
|
||||
If you've successfully used a model that's not listed here, please let us know by opening an issue or submitting a pull request to update this list.
|
||||
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
|
||||
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
|
||||
- **Quick Refactoring**: AI-assisted code improvements and alternative suggestions
|
||||
- **File Context**: Attach or link files for better AI understanding
|
||||
- **Tool Calling**: AI can read project files, search code, and access diagnostics
|
||||
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
|
||||
- **Customizable**: Project-specific rules and extensive model templates
|
||||
|
||||
## Development Progress
|
||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||
|
||||
- [x] Basic plugin with code autocomplete functionality
|
||||
- [ ] Improve and automate settings
|
||||
- [ ] Add chat functionality
|
||||
- [ ] Support for more providers and models
|
||||
<details>
|
||||
<summary>Code completion: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
|
||||
</details>
|
||||
|
||||
## Installation Plugin
|
||||
<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>
|
||||
|
||||
1. Install QtCreator 14.0
|
||||
2. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
|
||||
3. Install a language model in Ollama. For example, you can run:
|
||||
<details>
|
||||
<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">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Chat with LLM models in side panels: (click to expand)</summary>
|
||||
<img src="https://github.com/user-attachments/assets/ead5a5d9-b40a-4f17-af05-77fa2bcb3a61" width="600" alt="QodeAssistChat">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Chat with LLM models in bottom panel: (click to expand)</summary>
|
||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Chat in addtional window: (click to expand)</summary>
|
||||
<img width="851" height="865" alt="image" src="https://github.com/user-attachments/assets/a68894b7-886e-4501-a61b-7161ae34b427" />
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Automatic syncing with open editor files: (click to expand)</summary>
|
||||
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
|
||||
</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
|
||||
|
||||
### 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
|
||||
```
|
||||
ollama run starcoder2:7b
|
||||
```
|
||||
4. Download the QodeAssist plugin.
|
||||
5. Launch Qt Creator and install the plugin:
|
||||
- Go to MacOS: Qt Creator -> About Plugins...
|
||||
Windows\Linux: Help -> About Plugins...
|
||||
|
||||
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
||||
|
||||
### Method 2: Manual Installation
|
||||
|
||||
1. Install Latest Qt Creator
|
||||
2. Download the QodeAssist plugin for your Qt Creator
|
||||
- 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:
|
||||
- Go to:
|
||||
- MacOS: Qt Creator -> About Plugins...
|
||||
- Windows\Linux: Help -> About Plugins...
|
||||
- Click on "Install Plugin..."
|
||||
- Select the downloaded QodeAssist plugin archive file
|
||||
|
||||
## Configure Plugin
|
||||
## Configuration
|
||||
|
||||
1. Open Qt Creator settings
|
||||
2. Navigate to the "Qode Assist" tab
|
||||
3. Choose your LLM provider (e.g., Ollama)
|
||||
- If you haven't added the provider to your system PATH, specify the path to the provider executable in the "Provider Paths" field
|
||||
4. Select the installed model
|
||||
- If you need to enter the model name manually, it indicates that the plugin cannot locate the provider's executable file. However, this doesn't affect the plugin's functionality – it will still work correctly. This autoselector input option is provided for your convenience, allowing you to easily select and use different models
|
||||
5. Choose the prompt template that corresponds to your model
|
||||
6. Apply the settings
|
||||
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
|
||||
|
||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||
### Supported Providers
|
||||
|
||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
||||
- **[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
|
||||
- Fast code refactoring with AI assistance
|
||||
- Selection-based improvements
|
||||
- Alternative code suggestions
|
||||
- **[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)
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
| Qt Creator Version | QodeAssist Version |
|
||||
|-------------------|-------------------|
|
||||
| 17.0.0+ | 0.6.0 - 0.x.x |
|
||||
| 16.0.2 | 0.5.13 - 0.x.x |
|
||||
| 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
|
||||
|
||||
- [x] Code completion functionality
|
||||
- [x] Chat assistant with multiple panels
|
||||
- [x] Diff sharing with models
|
||||
- [x] Tools/function calling support
|
||||
- [x] Project-specific rules
|
||||
- [ ] Full project source sharing
|
||||
- [ ] Additional provider support
|
||||
- [ ] MCP (Model Context Protocol) support
|
||||
|
||||
## Support the development of QodeAssist
|
||||
If you find QodeAssist helpful, there are several ways you can support the project:
|
||||
@ -67,8 +228,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
|
||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||
|
||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following cryptocurrency addresses:
|
||||
|
||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||
@ -78,12 +238,44 @@ Every contribution, no matter how small, is greatly appreciated and helps keep t
|
||||
|
||||
## How to Build
|
||||
|
||||
Create a build directory and run
|
||||
### Prerequisites
|
||||
- CMake 3.16+
|
||||
- C++20 compatible compiler
|
||||
- Qt Creator development files
|
||||
|
||||
cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
|
||||
cmake --build .
|
||||
### Build Steps
|
||||
|
||||
1. Create a build directory:
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
### Code Style
|
||||
|
||||
- **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).
|
||||
|
||||

|
||||

|
||||
|
||||
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/`
|
||||
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
|
||||
relative or absolute path to this plugin directory.
|
||||
|
||||
14
TaskFlow/CMakeLists.txt
Normal file
@ -0,0 +1,14 @@
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(Editor)
|
||||
# add_subdirectory(serialization)
|
||||
# add_subdirectory(tasks)
|
||||
|
||||
qt_add_library(TaskFlow STATIC)
|
||||
|
||||
target_link_libraries(TaskFlow
|
||||
PUBLIC
|
||||
TaskFlowCore
|
||||
TaskFlowEditorplugin
|
||||
# TaskFlowSerialization
|
||||
# TaskFlowTasks
|
||||
)
|
||||
41
TaskFlow/Editor/CMakeLists.txt
Normal file
@ -0,0 +1,41 @@
|
||||
qt_add_library(TaskFlowEditor STATIC)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
qt_add_qml_module(TaskFlowEditor
|
||||
URI TaskFlow.Editor
|
||||
VERSION 1.0
|
||||
DEPENDENCIES QtQuick
|
||||
RESOURCES
|
||||
QML_FILES
|
||||
qml/FlowEditorView.qml
|
||||
qml/Flow.qml
|
||||
qml/Task.qml
|
||||
qml/TaskPort.qml
|
||||
qml/TaskParameter.qml
|
||||
qml/TaskConnection.qml
|
||||
SOURCES
|
||||
FlowEditor.hpp FlowEditor.cpp
|
||||
FlowsModel.hpp FlowsModel.cpp
|
||||
TaskItem.hpp TaskItem.cpp
|
||||
FlowItem.hpp FlowItem.cpp
|
||||
TaskModel.hpp TaskModel.cpp
|
||||
TaskPortItem.hpp TaskPortItem.cpp
|
||||
TaskPortModel.hpp TaskPortModel.cpp
|
||||
TaskConnectionsModel.hpp TaskConnectionsModel.cpp
|
||||
TaskConnectionItem.hpp TaskConnectionItem.cpp
|
||||
GridBackground.hpp GridBackground.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(TaskFlowEditor
|
||||
PUBLIC
|
||||
Qt::Quick
|
||||
PRIVATE
|
||||
TaskFlowCore
|
||||
)
|
||||
|
||||
target_include_directories(TaskFlowEditor
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
)
|
||||
120
TaskFlow/Editor/FlowEditor.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
|
||||
#include "FlowEditor.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowEditor::FlowEditor(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{}
|
||||
|
||||
void FlowEditor::initialize()
|
||||
{
|
||||
emit availableTaskTypesChanged();
|
||||
emit availableFlowsChanged();
|
||||
|
||||
m_flowsModel = new FlowsModel(m_flowManager, this);
|
||||
|
||||
emit flowsModelChanged();
|
||||
|
||||
if (m_flowsModel->rowCount() > 0) {
|
||||
setCurrentFlowIndex(0);
|
||||
}
|
||||
|
||||
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
|
||||
m_currentFlow = m_flowManager->getFlow();
|
||||
emit currentFlowChanged();
|
||||
}
|
||||
|
||||
QString FlowEditor::currentFlowId() const
|
||||
{
|
||||
return m_currentFlowId;
|
||||
}
|
||||
|
||||
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
|
||||
{
|
||||
if (m_currentFlowId == newCurrentFlowId)
|
||||
return;
|
||||
m_currentFlowId = newCurrentFlowId;
|
||||
emit currentFlowIdChanged();
|
||||
}
|
||||
|
||||
QStringList FlowEditor::availableTaskTypes() const
|
||||
{
|
||||
if (m_flowManager)
|
||||
return m_flowManager->getAvailableTasksTypes();
|
||||
else {
|
||||
return {"No flow manager"};
|
||||
}
|
||||
}
|
||||
|
||||
QStringList FlowEditor::availableFlows() const
|
||||
{
|
||||
if (m_flowManager) {
|
||||
auto flows = m_flowManager->getAvailableFlows();
|
||||
return flows.size() > 0 ? flows : QStringList{"No flows"};
|
||||
} else {
|
||||
return {"No flow manager"};
|
||||
}
|
||||
}
|
||||
|
||||
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
|
||||
{
|
||||
if (m_flowManager == newFlowManager)
|
||||
return;
|
||||
m_flowManager = newFlowManager;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
FlowsModel *FlowEditor::flowsModel() const
|
||||
{
|
||||
return m_flowsModel;
|
||||
}
|
||||
|
||||
int FlowEditor::currentFlowIndex() const
|
||||
{
|
||||
return m_currentFlowIndex;
|
||||
}
|
||||
|
||||
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
|
||||
{
|
||||
if (m_currentFlowIndex == newCurrentFlowIndex)
|
||||
return;
|
||||
m_currentFlowIndex = newCurrentFlowIndex;
|
||||
emit currentFlowIndexChanged();
|
||||
}
|
||||
|
||||
Flow *FlowEditor::getFlow(const QString &flowName)
|
||||
{
|
||||
return m_flowManager->getFlow(flowName);
|
||||
}
|
||||
|
||||
Flow *FlowEditor::getCurrentFlow()
|
||||
{
|
||||
return m_flowManager->getFlow(m_currentFlowId);
|
||||
}
|
||||
|
||||
Flow *FlowEditor::currentFlow() const
|
||||
{
|
||||
return m_currentFlow;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
86
TaskFlow/Editor/FlowEditor.hpp
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "FlowsModel.hpp"
|
||||
#include <FlowManager.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowEditor : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(
|
||||
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
|
||||
Q_PROPERTY(
|
||||
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
|
||||
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
|
||||
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
|
||||
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
|
||||
currentFlowIndexChanged)
|
||||
|
||||
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
|
||||
|
||||
public:
|
||||
FlowEditor(QQuickItem *parent = nullptr);
|
||||
|
||||
void initialize();
|
||||
|
||||
QString currentFlowId() const;
|
||||
void setCurrentFlowId(const QString &newCurrentFlowId);
|
||||
|
||||
QStringList availableTaskTypes() const;
|
||||
QStringList availableFlows() const;
|
||||
|
||||
void setFlowManager(FlowManager *newFlowManager);
|
||||
|
||||
FlowsModel *flowsModel() const;
|
||||
|
||||
int currentFlowIndex() const;
|
||||
void setCurrentFlowIndex(int newCurrentFlowIndex);
|
||||
|
||||
Q_INVOKABLE Flow *getFlow(const QString &flowName);
|
||||
Q_INVOKABLE Flow *getCurrentFlow();
|
||||
|
||||
Flow *currentFlow() const;
|
||||
|
||||
signals:
|
||||
void currentFlowIdChanged();
|
||||
void availableTaskTypesChanged();
|
||||
void availableFlowsChanged();
|
||||
void flowsModelChanged();
|
||||
|
||||
void currentFlowIndexChanged();
|
||||
|
||||
void currentFlowChanged();
|
||||
|
||||
private:
|
||||
FlowManager *m_flowManager = nullptr;
|
||||
QString m_currentFlowId;
|
||||
FlowsModel *m_flowsModel;
|
||||
int m_currentFlowIndex;
|
||||
Flow *m_currentFlow = nullptr;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
90
TaskFlow/Editor/FlowItem.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
#include "FlowItem.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowItem::FlowItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); });
|
||||
}
|
||||
|
||||
QString FlowItem::flowId() const
|
||||
{
|
||||
if (!m_flow)
|
||||
return {"no flow"};
|
||||
return m_flow->flowId();
|
||||
}
|
||||
|
||||
void FlowItem::setFlowId(const QString &newFlowId)
|
||||
{
|
||||
if (m_flow->flowId() == newFlowId)
|
||||
return;
|
||||
m_flow->setFlowId(newFlowId);
|
||||
emit flowIdChanged();
|
||||
}
|
||||
|
||||
Flow *FlowItem::flow() const
|
||||
{
|
||||
return m_flow;
|
||||
}
|
||||
|
||||
void FlowItem::setFlow(Flow *newFlow)
|
||||
{
|
||||
if (m_flow == newFlow)
|
||||
return;
|
||||
m_flow = newFlow;
|
||||
emit flowChanged();
|
||||
emit flowIdChanged();
|
||||
qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow;
|
||||
|
||||
m_taskModel = new TaskModel(m_flow, this);
|
||||
m_connectionsModel = new TaskConnectionsModel(m_flow, this);
|
||||
|
||||
emit taskModelChanged();
|
||||
emit connectionsModelChanged();
|
||||
}
|
||||
|
||||
TaskModel *FlowItem::taskModel() const
|
||||
{
|
||||
return m_taskModel;
|
||||
}
|
||||
|
||||
TaskConnectionsModel *FlowItem::connectionsModel() const
|
||||
{
|
||||
return m_connectionsModel;
|
||||
}
|
||||
|
||||
QVariantList FlowItem::taskItems() const
|
||||
{
|
||||
return m_taskItems;
|
||||
}
|
||||
|
||||
void FlowItem::setTaskItems(const QVariantList &newTaskItems)
|
||||
{
|
||||
qDebug() << "FlowItem::setTaskItems" << newTaskItems;
|
||||
if (m_taskItems == newTaskItems)
|
||||
return;
|
||||
m_taskItems = newTaskItems;
|
||||
emit taskItemsChanged();
|
||||
}
|
||||
|
||||
void FlowItem::updateFlowLayout()
|
||||
{
|
||||
auto allItems = this->childItems();
|
||||
|
||||
for (auto child : allItems) {
|
||||
if (child->objectName() == QString("TaskItem")) {
|
||||
qDebug() << "Found TaskItem:" << child;
|
||||
auto taskItem = qobject_cast<TaskItem *>(child);
|
||||
m_taskItemsList.insert(taskItem, taskItem->task());
|
||||
}
|
||||
|
||||
if (child->objectName() == QString("TaskConnectionItem")) {
|
||||
qDebug() << "Found TaskConnectionItem:" << child;
|
||||
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
|
||||
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
61
TaskFlow/Editor/FlowItem.hpp
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
|
||||
#include "TaskConnectionItem.hpp"
|
||||
#include "TaskConnectionsModel.hpp"
|
||||
#include "TaskItem.hpp"
|
||||
#include "TaskModel.hpp"
|
||||
#include <Flow.hpp>
|
||||
#include <TaskConnection.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged)
|
||||
Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged)
|
||||
Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged)
|
||||
Q_PROPERTY(
|
||||
TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged)
|
||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
||||
|
||||
public:
|
||||
explicit FlowItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QString flowId() const;
|
||||
void setFlowId(const QString &newFlowId);
|
||||
|
||||
Flow *flow() const;
|
||||
void setFlow(Flow *newFlow);
|
||||
|
||||
TaskModel *taskModel() const;
|
||||
|
||||
TaskConnectionsModel *connectionsModel() const;
|
||||
|
||||
QVariantList taskItems() const;
|
||||
void setTaskItems(const QVariantList &newTaskItems);
|
||||
|
||||
void updateFlowLayout();
|
||||
|
||||
signals:
|
||||
void flowIdChanged();
|
||||
void flowChanged();
|
||||
void taskModelChanged();
|
||||
void connectionsModelChanged();
|
||||
void taskItemsChanged();
|
||||
|
||||
private:
|
||||
Flow *m_flow = nullptr;
|
||||
TaskModel *m_taskModel = nullptr;
|
||||
TaskConnectionsModel *m_connectionsModel = nullptr;
|
||||
QVariantList m_taskItems;
|
||||
|
||||
QHash<TaskItem *, BaseTask *> m_taskItemsList;
|
||||
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
54
TaskFlow/Editor/FlowsModel.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
#include "FlowsModel.hpp"
|
||||
|
||||
#include "FlowManager.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_flowManager(flowManager)
|
||||
{
|
||||
connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded);
|
||||
}
|
||||
|
||||
int FlowsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_flowManager->flows().size();
|
||||
}
|
||||
|
||||
QVariant FlowsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size())
|
||||
return QVariant();
|
||||
|
||||
const auto flows = m_flowManager->flows().values();
|
||||
|
||||
switch (role) {
|
||||
case FlowRoles::FlowIdRole:
|
||||
return flows.at(index.row())->flowId();
|
||||
case FlowRoles::FlowDataRole:
|
||||
return QVariant::fromValue(flows.at(index.row()));
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FlowsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[FlowRoles::FlowIdRole] = "flowId";
|
||||
roles[FlowRoles::FlowDataRole] = "flowData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
void FlowsModel::onFlowAdded(const QString &flowId)
|
||||
{
|
||||
// qDebug() << "FlowsModel::Flow added: " << flowId;
|
||||
// int newIndex = m_flowManager->flows().size();
|
||||
// beginInsertRows(QModelIndex(), newIndex, newIndex);
|
||||
// endInsertRows();
|
||||
}
|
||||
|
||||
void FlowsModel::onFlowRemoved(const QString &flowId) {}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
31
TaskFlow/Editor/FlowsModel.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
|
||||
// #include "tasks/Flow.hpp"
|
||||
#include <FlowManager.hpp>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class FlowsModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole };
|
||||
|
||||
FlowsModel(FlowManager *flowManager, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
public slots:
|
||||
void onFlowAdded(const QString &flowId);
|
||||
void onFlowRemoved(const QString &flowId);
|
||||
|
||||
private:
|
||||
FlowManager *m_flowManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
98
TaskFlow/Editor/GridBackground.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "GridBackground.hpp"
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QQuickWindow>
|
||||
#include <QSGSimpleRectNode>
|
||||
#include <QSGSimpleTextureNode>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
GridBackground::GridBackground(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setFlag(QQuickItem::ItemHasContents, true);
|
||||
}
|
||||
|
||||
int GridBackground::gridSize() const
|
||||
{
|
||||
return m_gridSize;
|
||||
}
|
||||
|
||||
void GridBackground::setGridSize(int size)
|
||||
{
|
||||
if (m_gridSize != size) {
|
||||
m_gridSize = size;
|
||||
update();
|
||||
emit gridSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QColor GridBackground::gridColor() const
|
||||
{
|
||||
return m_gridColor;
|
||||
}
|
||||
|
||||
void GridBackground::setGridColor(const QColor &color)
|
||||
{
|
||||
if (m_gridColor != color) {
|
||||
m_gridColor = color;
|
||||
update();
|
||||
emit gridColorChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
|
||||
{
|
||||
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
|
||||
if (!node) {
|
||||
node = new QSGSimpleTextureNode();
|
||||
}
|
||||
|
||||
QPixmap pixmap(width(), height());
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
QPen pen(m_gridColor);
|
||||
pen.setWidth(1);
|
||||
painter.setPen(pen);
|
||||
painter.setOpacity(this->opacity());
|
||||
|
||||
for (int x = 0; x < width(); x += m_gridSize) {
|
||||
painter.drawLine(x, 0, x, height());
|
||||
}
|
||||
|
||||
for (int y = 0; y < height(); y += m_gridSize) {
|
||||
painter.drawLine(0, y, width(), y);
|
||||
}
|
||||
|
||||
painter.end();
|
||||
|
||||
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
|
||||
node->setTexture(texture);
|
||||
node->setRect(boundingRect());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
57
TaskFlow/Editor/GridBackground.hpp
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QPainter>
|
||||
#include <QQuickItem>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class GridBackground : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
|
||||
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
|
||||
|
||||
public:
|
||||
explicit GridBackground(QQuickItem *parent = nullptr);
|
||||
|
||||
int gridSize() const;
|
||||
void setGridSize(int size);
|
||||
|
||||
QColor gridColor() const;
|
||||
void setGridColor(const QColor &color);
|
||||
|
||||
signals:
|
||||
void gridSizeChanged();
|
||||
void gridColorChanged();
|
||||
|
||||
protected:
|
||||
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
|
||||
|
||||
private:
|
||||
int m_gridSize = 20;
|
||||
QColor m_gridColor = QColor(128, 128, 128);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
153
TaskFlow/Editor/TaskConnectionItem.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
#include "TaskConnectionItem.hpp"
|
||||
#include "TaskItem.hpp"
|
||||
#include "TaskPortItem.hpp"
|
||||
#include <QDebug>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskConnectionItem::TaskConnectionItem(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
{
|
||||
setObjectName("TaskConnectionItem");
|
||||
}
|
||||
|
||||
void TaskConnectionItem::setConnection(TaskConnection *connection)
|
||||
{
|
||||
if (m_connection == connection)
|
||||
return;
|
||||
|
||||
m_connection = connection;
|
||||
emit connectionChanged();
|
||||
|
||||
calculatePositions();
|
||||
}
|
||||
|
||||
void TaskConnectionItem::updatePositions()
|
||||
{
|
||||
// calculatePositions();
|
||||
}
|
||||
|
||||
void TaskConnectionItem::calculatePositions()
|
||||
{
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find source task item
|
||||
QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask());
|
||||
QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask());
|
||||
|
||||
if (!sourceTaskItem || !targetTaskItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find port items within tasks
|
||||
QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort());
|
||||
QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort());
|
||||
|
||||
if (!sourcePortItem || !targetPortItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate global positions
|
||||
QPointF sourceGlobal
|
||||
= sourcePortItem
|
||||
->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2);
|
||||
QPointF targetGlobal
|
||||
= targetPortItem
|
||||
->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2);
|
||||
|
||||
if (m_startPoint != sourceGlobal) {
|
||||
m_startPoint = sourceGlobal;
|
||||
emit startPointChanged();
|
||||
}
|
||||
|
||||
if (m_endPoint != targetGlobal) {
|
||||
m_endPoint = targetGlobal;
|
||||
emit endPointChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task)
|
||||
{
|
||||
for (const QVariant &item : m_taskItems) {
|
||||
QQuickItem *taskItem = qvariant_cast<QQuickItem *>(item);
|
||||
if (!taskItem)
|
||||
continue;
|
||||
|
||||
QVariant taskProp = taskItem->property("task");
|
||||
if (taskProp.isValid() && taskProp.value<BaseTask *>() == task) {
|
||||
return taskItem;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task)
|
||||
{
|
||||
// Проверяем objectName и task property
|
||||
if (item->objectName() == "TaskItem") {
|
||||
QVariant taskProp = item->property("task");
|
||||
if (taskProp.isValid()) {
|
||||
BaseTask *itemTask = taskProp.value<BaseTask *>();
|
||||
if (itemTask == task) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рекурсивно ищем в детях
|
||||
auto children = item->childItems();
|
||||
|
||||
for (QQuickItem *child : children) {
|
||||
if (QQuickItem *found = findTaskItemRecursive(child, task)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port)
|
||||
{
|
||||
std::function<QQuickItem *(QQuickItem *)> findPortRecursive =
|
||||
[&](QQuickItem *item) -> QQuickItem * {
|
||||
// Проверяем objectName и port property
|
||||
if (item->objectName() == "TaskPortItem") {
|
||||
QVariant portProp = item->property("port");
|
||||
if (portProp.isValid()) {
|
||||
TaskPort *itemPort = portProp.value<TaskPort *>();
|
||||
if (itemPort == port) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рекурсивно ищем в детях
|
||||
for (QQuickItem *child : item->childItems()) {
|
||||
if (QQuickItem *found = findPortRecursive(child)) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
return findPortRecursive(taskItem);
|
||||
}
|
||||
|
||||
QVariantList TaskConnectionItem::taskItems() const
|
||||
{
|
||||
return m_taskItems;
|
||||
}
|
||||
|
||||
void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems)
|
||||
{
|
||||
if (m_taskItems == newTaskItems)
|
||||
return;
|
||||
m_taskItems = newTaskItems;
|
||||
emit taskItemsChanged();
|
||||
|
||||
calculatePositions();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
55
TaskFlow/Editor/TaskConnectionItem.hpp
Normal file
@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "TaskConnection.hpp"
|
||||
#include <QPointF>
|
||||
#include <QQuickItem>
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
class TaskConnectionItem : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged)
|
||||
Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged)
|
||||
Q_PROPERTY(
|
||||
TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
||||
|
||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
||||
|
||||
public:
|
||||
TaskConnectionItem(QQuickItem *parent = nullptr);
|
||||
|
||||
QPointF startPoint() const { return m_startPoint; }
|
||||
QPointF endPoint() const { return m_endPoint; }
|
||||
|
||||
TaskConnection *connection() const { return m_connection; }
|
||||
void setConnection(TaskConnection *connection);
|
||||
|
||||
Q_INVOKABLE void updatePositions();
|
||||
|
||||
QVariantList taskItems() const;
|
||||
void setTaskItems(const QVariantList &newTaskItems);
|
||||
|
||||
signals:
|
||||
void startPointChanged();
|
||||
void endPointChanged();
|
||||
void connectionChanged();
|
||||
|
||||
void taskItemsChanged();
|
||||
|
||||
private:
|
||||
void calculatePositions();
|
||||
QQuickItem *findTaskItem(BaseTask *task);
|
||||
QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task);
|
||||
QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port);
|
||||
|
||||
private:
|
||||
TaskConnection *m_connection = nullptr;
|
||||
QPointF m_startPoint;
|
||||
QPointF m_endPoint;
|
||||
QVariantList m_taskItems;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||
29
TaskFlow/Editor/TaskConnectionsModel.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include "TaskConnectionsModel.hpp"
|
||||
|
||||
namespace QodeAssist::TaskFlow {
|
||||
|
||||
TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
, m_flow(flow)
|
||||
{}
|
||||
|
||||
int TaskConnectionsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return m_flow->connections().size();
|
||||
}
|
||||
|
||||
QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == TaskConnectionsRoles::TaskConnectionsRole)
|
||||
return QVariant::fromValue(m_flow->connections().at(index.row()));
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> TaskConnectionsModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
|
||||
return roles;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::TaskFlow
|
||||