feat: Add Gemini AI settings and inline action buttons

- Introduced GeminiSettings component for configuring Gemini AI settings including API key, model selection, and inline suggestions.
- Updated App.vue to include GeminiSettings in the view management.
- Enhanced MonacoEditor with AI action buttons for code fixing, explaining, refactoring, and optimizing.
- Implemented responsive design for GeminiSettings and MonacoEditor components.
- Added sidebar button to toggle Gemini settings.
- Integrated API calls for saving and testing Gemini configuration.
This commit is contained in:
Shni
2025-11-04 04:40:39 -06:00
parent 38046e4df8
commit 4cf99a6f91
8 changed files with 1618 additions and 35 deletions

View File

@@ -14,12 +14,15 @@ version = "0.1.0"
dependencies = [
"discord-rich-presence",
"regex",
"reqwest 0.11.27",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tokio",
"uuid 1.18.1",
]
[[package]]
@@ -526,6 +529,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -549,9 +562,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@@ -562,7 +575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
"core-foundation 0.10.1",
"libc",
]
@@ -862,7 +875,7 @@ dependencies = [
"rustc_version",
"toml 0.9.8",
"vswhom",
"winreg",
"winreg 0.55.0",
]
[[package]]
@@ -871,6 +884,15 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.0"
@@ -993,6 +1015,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1000,7 +1031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@@ -1014,6 +1045,12 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -1424,6 +1461,25 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "h2"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1472,6 +1528,17 @@ dependencies = [
"match_token",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.3.1"
@@ -1483,6 +1550,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -1490,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
"http 1.3.1",
]
[[package]]
@@ -1501,8 +1579,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -1512,6 +1590,36 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.7.0"
@@ -1522,8 +1630,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
@@ -1533,6 +1641,19 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.17"
@@ -1544,14 +1665,14 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.7.0",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
@@ -2055,6 +2176,23 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2429,6 +2567,50 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -3030,6 +3212,46 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
]
[[package]]
name = "reqwest"
version = "0.12.24"
@@ -3040,10 +3262,10 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper 1.7.0",
"hyper-util",
"js-sys",
"log",
@@ -3052,7 +3274,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tokio-util",
"tower",
@@ -3112,6 +3334,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -3133,6 +3364,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "schemars"
version = "0.8.22"
@@ -3196,6 +3436,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "selectors"
version = "0.24.0"
@@ -3450,6 +3713,16 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.1"
@@ -3469,7 +3742,7 @@ dependencies = [
"bytemuck",
"cfg_aliases",
"core-graphics",
"foreign-types",
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2 0.5.2",
@@ -3584,6 +3857,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -3604,6 +3883,27 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3625,7 +3925,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.10.0",
"block2 0.6.2",
"core-foundation",
"core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch",
@@ -3690,7 +3990,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http",
"http 1.3.1",
"jni",
"libc",
"log",
@@ -3704,7 +4004,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.24",
"serde",
"serde_json",
"serde_repr",
@@ -3876,7 +4176,7 @@ dependencies = [
"cookie",
"dpi",
"gtk",
"http",
"http 1.3.1",
"jni",
"objc2 0.6.3",
"objc2-ui-kit",
@@ -3899,7 +4199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93"
dependencies = [
"gtk",
"http",
"http 1.3.1",
"jni",
"log",
"objc2 0.6.3",
@@ -3932,7 +4232,7 @@ dependencies = [
"dunce",
"glob",
"html5ever",
"http",
"http 1.3.1",
"infer",
"json-patch",
"kuchikiki",
@@ -4081,13 +4381,36 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.6.1",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
@@ -4206,7 +4529,7 @@ dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
@@ -4221,8 +4544,8 @@ dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
"http",
"http-body",
"http 1.3.1",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower",
@@ -4434,6 +4757,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.1"
@@ -4921,6 +5250,24 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4963,6 +5310,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -5020,6 +5382,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -5038,6 +5406,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -5056,6 +5430,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -5086,6 +5466,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -5104,6 +5490,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -5122,6 +5514,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -5140,6 +5538,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -5170,6 +5574,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
@@ -5208,7 +5622,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http",
"http 1.3.1",
"javascriptcore-rs",
"jni",
"kuchikiki",

View File

@@ -25,4 +25,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
discord-rich-presence = "0.2"
regex = "1"
reqwest = { version = "0.11", features = ["json", "blocking"] }
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }

View File

@@ -8,6 +8,29 @@ use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
// Cliente Discord RPC global
static DISCORD_CLIENT: Mutex<Option<DiscordIpcClient>> = Mutex::new(None);
// Structs para Codeium API
#[derive(Debug, Serialize, Deserialize)]
struct CodeiumCompletionRequest {
text: String,
cursor_position: usize,
language: String,
file_path: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct CodeiumCompletion {
text: String,
range: CodeiumRange,
}
#[derive(Debug, Serialize, Deserialize)]
struct CodeiumRange {
start_line: usize,
start_column: usize,
end_line: usize,
end_column: usize,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FileInfo {
@@ -749,6 +772,397 @@ fn get_package_scripts(project_root: String) -> Result<Vec<(String, String)>, St
Ok(scripts)
}
// ============================================
// GEMINI API INTEGRATION (Code Completion)
// ============================================
#[derive(serde::Serialize)]
struct GeminiRequest {
contents: Vec<GeminiContent>,
#[serde(rename = "generationConfig")]
generation_config: GeminiGenerationConfig,
}
#[derive(serde::Serialize)]
struct GeminiContent {
parts: Vec<GeminiPart>,
}
#[derive(serde::Serialize)]
struct GeminiPart {
text: String,
}
#[derive(serde::Serialize)]
struct GeminiGenerationConfig {
temperature: f32,
#[serde(rename = "maxOutputTokens")]
max_output_tokens: i32,
#[serde(rename = "candidateCount")]
candidate_count: i32,
#[serde(rename = "thinkingConfig", skip_serializing_if = "Option::is_none")]
thinking_config: Option<GeminiThinkingConfig>,
}
#[derive(serde::Serialize)]
struct GeminiThinkingConfig {
#[serde(rename = "thinkingBudget")]
thinking_budget: i32,
#[serde(rename = "includeThoughts")]
include_thoughts: bool,
}
#[derive(serde::Deserialize)]
struct GeminiResponse {
candidates: Option<Vec<GeminiCandidate>>,
}
#[derive(serde::Deserialize)]
struct GeminiCandidate {
content: GeminiResponseContent,
}
#[derive(serde::Deserialize)]
struct GeminiResponseContent {
parts: Vec<GeminiResponsePart>,
}
#[derive(serde::Deserialize)]
struct GeminiResponsePart {
text: String,
}
#[tauri::command]
async fn get_gemini_completion(
text: String,
cursor_position: usize,
language: String,
file_path: String,
api_key: String,
model: String,
agent_mode: Option<bool>,
) -> Result<Vec<String>, String> {
use reqwest::Client;
if api_key.is_empty() {
return Ok(vec![]);
}
// Extraer contexto antes y después del cursor
let before = text.chars().take(cursor_position).collect::<String>();
let after = text.chars().skip(cursor_position).take(200).collect::<String>();
// Tomar últimas 15 líneas de contexto
let context_lines: Vec<&str> = before.lines().rev().take(15).collect();
let context = context_lines.into_iter().rev().collect::<Vec<&str>>().join("\n");
// Crear prompt optimizado para autocompletado
let prompt = format!(
"You are a code completion AI. Complete the {} code at the cursor position.
File: {}
Code:
```{}
{}[CURSOR]{}
```
Complete ONLY what comes immediately after [CURSOR]. Output raw code only, no markdown, no explanations:",
language, file_path, language, context, after
);
// Configurar thinking mode basado en agent_mode
let (thinking_config, max_tokens) = if agent_mode.unwrap_or(false) {
println!("🤖 Modo Agent activado con thinking dinámico");
(
Some(GeminiThinkingConfig {
thinking_budget: -1, // Dinámico: el modelo decide cuánto "pensar"
include_thoughts: true, // Incluir resumen de pensamientos
}),
512 // Más tokens cuando thinking está activado
)
} else {
println!("⚡ Modo rápido: thinking desactivado");
(
Some(GeminiThinkingConfig {
thinking_budget: 0, // Sin thinking para completions rápidos
include_thoughts: false,
}),
120 // Tokens normales para autocompletado rápido
)
};
let request_body = GeminiRequest {
contents: vec![GeminiContent {
parts: vec![GeminiPart { text: prompt }],
}],
generation_config: GeminiGenerationConfig {
temperature: 0.2,
max_output_tokens: max_tokens,
candidate_count: 1,
thinking_config,
},
};
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent?key={}",
model, api_key
);
println!("🚀 Llamando a Gemini API...");
println!(" Modelo: {}", model);
println!(" URL: {}", url.split("?key=").next().unwrap_or(""));
let client = Client::new();
match client
.post(&url)
.header("Content-Type", "application/json")
.json(&request_body)
.timeout(std::time::Duration::from_secs(8))
.send()
.await
{
Ok(response) => {
let status = response.status();
println!("📡 Status: {}", status);
if response.status().is_success() {
// Primero obtener el texto para debuggear
let response_text = response.text().await.map_err(|e| e.to_string())?;
println!("📦 Respuesta completa: {}", response_text);
// Intentar parsear manualmente
match serde_json::from_str::<serde_json::Value>(&response_text) {
Ok(json) => {
println!("✅ JSON parseado correctamente");
let mut suggestions = Vec::new();
// Navegar por la estructura real de Gemini
if let Some(candidates) = json["candidates"].as_array() {
println!("✅ Candidatos encontrados: {}", candidates.len());
for (i, candidate) in candidates.iter().enumerate() {
println!(" 📋 Procesando candidato #{}", i + 1);
// Ver el finishReason
if let Some(finish_reason) = candidate["finishReason"].as_str() {
println!(" 🏁 Finish reason: {}", finish_reason);
}
// Intentar extraer el texto de diferentes formas
if let Some(content) = candidate.get("content") {
if let Some(parts) = content["parts"].as_array() {
println!(" ✅ Parts encontrados: {}", parts.len());
for part in parts {
// Detectar si es un "thought" (pensamiento del modelo)
let is_thought = part["thought"].as_bool().unwrap_or(false);
if let Some(text) = part["text"].as_str() {
if is_thought {
println!(" 💭 Pensamiento del modelo: {}", text);
// Los pensamientos NO se agregan como sugerencias
continue;
}
let cleaned = text.trim()
.trim_start_matches("```")
.trim_start_matches(&language)
.trim_end_matches("```")
.trim();
if !cleaned.is_empty() {
println!(" ✨ Sugerencia: {}", cleaned);
suggestions.push(cleaned.to_string());
}
}
}
} else {
println!(" ⚠️ No hay 'parts' en content");
}
} else {
println!(" ⚠️ No hay 'content' en candidate");
}
}
}
if suggestions.is_empty() {
println!("⚠️ No se pudieron extraer sugerencias del JSON");
}
Ok(suggestions)
}
Err(e) => {
eprintln!("❌ Error parsing JSON: {:?}", e);
Ok(vec![])
}
}
} else {
let error_text = response.text().await.unwrap_or_default();
eprintln!("❌ Gemini API error {}: {}", status, error_text);
Ok(vec![])
}
}
Err(e) => {
eprintln!("❌ Error calling Gemini: {:?}", e);
Ok(vec![])
}
}
}
// Nueva función para procesar prompts directos (Fix, Explain, etc.)
#[tauri::command]
async fn ask_gemini(
prompt: String,
api_key: String,
model: String,
use_thinking: bool,
) -> Result<String, String> {
use reqwest::Client;
if api_key.is_empty() {
return Err("No API key provided".to_string());
}
// Configurar thinking mode
let (thinking_config, max_tokens) = if use_thinking {
println!("🤖 ask_gemini con thinking activado");
(
Some(GeminiThinkingConfig {
thinking_budget: -1,
include_thoughts: true,
}),
2048 // Más tokens para respuestas completas
)
} else {
println!("⚡ ask_gemini modo rápido");
(
Some(GeminiThinkingConfig {
thinking_budget: 0,
include_thoughts: false,
}),
1024
)
};
let request_body = GeminiRequest {
contents: vec![GeminiContent {
parts: vec![GeminiPart { text: prompt }],
}],
generation_config: GeminiGenerationConfig {
temperature: 0.3,
max_output_tokens: max_tokens,
candidate_count: 1,
thinking_config,
},
};
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent?key={}",
model, api_key
);
println!("🚀 ask_gemini llamando a Gemini API...");
println!(" Modelo: {}", model);
let client = Client::new();
match client
.post(&url)
.header("Content-Type", "application/json")
.json(&request_body)
.timeout(std::time::Duration::from_secs(15))
.send()
.await
{
Ok(response) => {
let status = response.status();
println!("📡 Status: {}", status);
if response.status().is_success() {
let response_text = response.text().await.map_err(|e| e.to_string())?;
match serde_json::from_str::<serde_json::Value>(&response_text) {
Ok(json) => {
if let Some(candidates) = json["candidates"].as_array() {
if let Some(candidate) = candidates.first() {
if let Some(content) = candidate.get("content") {
if let Some(parts) = content["parts"].as_array() {
// Concatenar todas las partes de texto (ignorando thoughts)
let mut result = String::new();
for part in parts {
let is_thought = part["thought"].as_bool().unwrap_or(false);
if !is_thought {
if let Some(text) = part["text"].as_str() {
result.push_str(text);
}
}
}
if !result.is_empty() {
println!("✅ Respuesta obtenida: {} caracteres", result.len());
return Ok(result);
}
}
}
}
}
Err("No se encontró contenido en la respuesta".to_string())
}
Err(e) => {
eprintln!("❌ Error parsing JSON: {:?}", e);
Err(format!("Error parsing response: {}", e))
}
}
} else {
let error_text = response.text().await.unwrap_or_default();
eprintln!("❌ Gemini API error {}: {}", status, error_text);
Err(format!("API error: {}", error_text))
}
}
Err(e) => {
eprintln!("❌ Error calling Gemini: {:?}", e);
Err(format!("Network error: {}", e))
}
}
}
// Guardar configuración de Gemini
#[tauri::command]
fn save_gemini_config(api_key: String, model: String, app_data_dir: String, agent_mode: Option<bool>, inline_suggestions_enabled: Option<bool>) -> Result<(), String> {
// Crear el directorio si no existe
let dir_path = Path::new(&app_data_dir);
if !dir_path.exists() {
fs::create_dir_all(dir_path).map_err(|e| format!("Error creando directorio: {}", e))?;
}
let config_path = dir_path.join("gemini_config.json");
let config = serde_json::json!({
"api_key": api_key,
"model": model,
"enabled": true,
"agent_mode": agent_mode.unwrap_or(false),
"inline_suggestions_enabled": inline_suggestions_enabled.unwrap_or(false)
});
fs::write(&config_path, serde_json::to_string_pretty(&config).unwrap())
.map_err(|e| format!("Error guardando archivo: {}", e))?;
Ok(())
}
// Leer configuración de Gemini
#[tauri::command]
fn load_gemini_config(app_data_dir: String) -> Result<String, String> {
let config_path = Path::new(&app_data_dir).join("gemini_config.json");
if !config_path.exists() {
return Err("No hay API key configurada".to_string());
}
let content = fs::read_to_string(&config_path).map_err(|e| e.to_string())?;
Ok(content)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@@ -778,7 +1192,11 @@ pub fn run() {
write_env_file,
scan_env_variables,
scan_env_variables_with_locations,
get_package_scripts
get_package_scripts,
get_gemini_completion,
ask_gemini,
save_gemini_config,
load_gemini_config
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -10,6 +10,7 @@ import CommandPalette from "./components/CommandPalette.vue";
import SkeletonLoader from "./components/SkeletonLoader.vue";
import DatabaseViewer from "./components/DatabaseViewer.vue";
import EnvManager from "./components/EnvManager.vue";
import GeminiSettings from "./components/GeminiSettings.vue";
import type { ProjectStats, FileInfo, Command, Event } from "./types/bot";
// Estado de la aplicación
@@ -31,7 +32,7 @@ const events = ref<FileInfo[]>([]);
const allFiles = ref<FileInfo[]>([]);
const selectedFile = ref<FileInfo | null>(null);
const fileContent = ref<string>("");
const currentView = ref<"editor" | "command-creator" | "event-creator" | "database" | "env-manager">("editor");
const currentView = ref<"editor" | "command-creator" | "event-creator" | "database" | "env-manager" | "gemini-settings">("editor");
const loading = ref(false);
const errorMsg = ref<string>("");
const schemaContent = ref<string>("");
@@ -340,6 +341,17 @@ function toggleEnvManager() {
}
}
// Toggle Gemini Settings
function toggleGeminiSettings() {
if (currentView.value === 'gemini-settings') {
currentView.value = 'editor';
updateDiscordRPC("Navegando proyecto", "En el editor");
} else {
currentView.value = 'gemini-settings';
updateDiscordRPC("Configurando Gemini AI", "Google Gemini Assistant");
}
}
// Guardar schema de base de datos
async function saveSchema(content: string) {
try {
@@ -423,6 +435,7 @@ function handlePaletteCommand(commandId: string) {
@toggle-dev-ultra="toggleDevUltra"
@toggle-database="toggleDatabase"
@toggle-env-manager="toggleEnvManager"
@toggle-gemini-settings="toggleGeminiSettings"
@notify="showNotification"
/>
@@ -474,6 +487,11 @@ function handlePaletteCommand(commandId: string) {
@notify="showNotification"
/>
<!-- Gemini Settings -->
<GeminiSettings
v-if="currentView === 'gemini-settings'"
/>
<!-- Welcome Screen -->
<div v-if="currentView === 'editor' && !selectedFile" class="welcome-screen">
<div class="welcome-content">
@@ -719,4 +737,139 @@ body {
opacity: 1;
}
}
/* ============================================
RESPONSIVE DESIGN - Media Queries
============================================ */
/* Pantallas grandes (1920px+) */
@media (min-width: 1920px) {
.welcome-content h1 {
font-size: 64px;
}
.welcome-content > p {
font-size: 22px;
}
.stat-number {
font-size: 56px;
}
.stat-label {
font-size: 16px;
}
}
/* Pantallas medianas-grandes (1366px - 1919px) */
@media (min-width: 1366px) and (max-width: 1919px) {
.welcome-content h1 {
font-size: 52px;
}
.welcome-stats {
gap: 32px;
}
}
/* Tabletas y pantallas pequeñas (768px - 1365px) */
@media (max-width: 1365px) {
.app-container {
flex-direction: column;
}
.welcome-content h1 {
font-size: 40px;
}
.welcome-content > p {
font-size: 16px;
}
.welcome-stats {
flex-direction: column;
gap: 20px;
}
.stat-number {
font-size: 40px;
}
}
/* Móviles y pantallas muy pequeñas (< 768px) */
@media (max-width: 767px) {
.app-container {
flex-direction: column;
}
.welcome-content {
padding: 20px;
}
.welcome-content h1 {
font-size: 32px;
}
.welcome-content > p {
font-size: 14px;
margin-bottom: 24px;
}
.stat-number {
font-size: 32px;
}
.stat-label {
font-size: 12px;
}
.notification {
top: 10px;
right: 10px;
left: 10px;
font-size: 13px;
padding: 10px 16px;
}
.error-banner {
font-size: 12px;
padding: 10px 16px;
}
}
/* Ajustes para pantallas ultra-wide (2560px+) */
@media (min-width: 2560px) {
.welcome-content {
max-width: 800px;
}
.welcome-content h1 {
font-size: 72px;
}
.welcome-content > p {
font-size: 24px;
}
}
/* Ajustes de altura para pantallas cortas */
@media (max-height: 700px) {
.welcome-content {
padding: 20px;
}
.welcome-content h1 {
font-size: 36px;
margin-bottom: 12px;
}
.welcome-content > p {
font-size: 16px;
margin-bottom: 24px;
}
.welcome-stats {
margin-bottom: 24px;
}
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="gemini-settings">
<div class="settings-header">
<h3> Configuración de Gemini AI</h3>
<p class="subtitle">Autocompletado inteligente con Google Gemini</p>
<div class="info-box">
<p><strong>💡 Cómo usar Gemini:</strong></p>
<ul>
<li><strong>Inline Suggestions:</strong> Escribe código y aparecerán sugerencias en gris. Presiona <kbd>Tab</kbd> para aceptar.</li>
<li><strong>Modo Thinking:</strong> Activa para código complejo. El modelo "piensa" antes de sugerir (más lento pero más preciso).</li>
</ul>
</div>
</div> <div class="settings-content">
<div class="status-badge" :class="{ active: isConfigured, inactive: !isConfigured }">
<span class="status-dot"></span>
{{ isConfigured ? '✓ Configurado' : '○ No configurado' }}
</div>
<div class="form-group">
<label for="model">Modelo de IA</label>
<select id="model" v-model="selectedModel" class="model-select" @change="hasChanges = true">
<option value="gemini-2.5-flash"> Gemini 2.5 Flash (Rápido)</option>
<option value="gemini-2.5-pro">🚀 Gemini 2.5 Pro (Potente)</option>
<option value="gemini-1.5-flash"> Gemini 1.5 Flash (Legacy)</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="agentMode" @change="hasChanges = true" />
<span class="checkbox-text">
<strong>🧠 Modo Thinking (Experimental)</strong>
<small>El modelo razona internamente antes de sugerir código (más tokens, más lento)</small>
</span>
</label>
</div>
<div class="form-group">
<label class="checkbox-label warning-label">
<input type="checkbox" v-model="inlineSuggestionsEnabled" @change="hasChanges = true" />
<span class="checkbox-text">
<strong> Autocompletado Inline (Alto consumo de tokens)</strong>
<small>
<span class="warning-text"> ADVERTENCIA:</span> Genera sugerencias automáticamente mientras escribes.
Cada pausa de 500ms hace una llamada a la API. Puede consumir muchos tokens rápidamente.
<br><strong>Recomendación:</strong> Usa solo los botones de acción (Fix, Explain, etc.) para ahorrar tokens.
</small>
</span>
</label>
</div>
<div class="form-group">
<label for="apiKey">API Key de Google</label>
<div class="input-wrapper">
<input id="apiKey" v-model="apiKey" :type="showApiKey ? 'text' : 'password'" placeholder="Ingresa tu API key" class="api-key-input" @input="hasChanges = true" />
<button class="toggle-visibility-btn" @click="showApiKey = !showApiKey" type="button">{{ showApiKey ? '👁️' : '👁️‍🗨️' }}</button>
</div>
<span class="helper-text">
<a href="https://aistudio.google.com/app/apikey" target="_blank" class="link">Obtén tu API key gratis aquí</a>
</span>
</div>
<div class="actions">
<button class="btn btn-primary" :disabled="!hasChanges || !apiKey" @click="saveSettings">💾 Guardar</button>
<button v-if="isConfigured" class="btn btn-test" @click="testConnection" :disabled="testing">{{ testing ? ' Probando...' : '🧪 Probar' }}</button>
</div>
<div v-if="message" class="message" :class="messageType">{{ message }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { appDataDir } from '@tauri-apps/api/path';
const apiKey = ref('');
const showApiKey = ref(false);
const selectedModel = ref('gemini-2.5-flash');
const agentMode = ref(false);
const inlineSuggestionsEnabled = ref(false);
const hasChanges = ref(false);
const isConfigured = ref(false);
const testing = ref(false);
const message = ref('');
const messageType = ref<'success' | 'error' | 'info'>('info');
onMounted(async () => {
try {
const dataDir = await appDataDir();
const configJson = await invoke<string>('load_gemini_config', { appDataDir: dataDir });
const config = JSON.parse(configJson);
if (config.api_key) {
apiKey.value = config.api_key;
selectedModel.value = config.model || 'gemini-2.5-flash';
agentMode.value = config.agent_mode || false;
inlineSuggestionsEnabled.value = config.inline_suggestions_enabled || false;
isConfigured.value = true;
localStorage.setItem('gemini_api_key', config.api_key);
localStorage.setItem('gemini_model', config.model);
localStorage.setItem('gemini_agent_mode', config.agent_mode ? 'true' : 'false');
localStorage.setItem('gemini_inline_suggestions', config.inline_suggestions_enabled ? 'true' : 'false');
}
} catch (error) {
console.log('No hay configuración previa');
}
});
async function saveSettings() {
if (!apiKey.value) {
showMessage('Por favor ingresa una API key', 'error');
return;
}
try {
const dataDir = await appDataDir();
await invoke('save_gemini_config', {
apiKey: apiKey.value,
model: selectedModel.value,
appDataDir: dataDir,
agentMode: agentMode.value,
inlineSuggestionsEnabled: inlineSuggestionsEnabled.value
});
localStorage.setItem('gemini_api_key', apiKey.value);
localStorage.setItem('gemini_model', selectedModel.value);
localStorage.setItem('gemini_agent_mode', agentMode.value ? 'true' : 'false');
localStorage.setItem('gemini_inline_suggestions', inlineSuggestionsEnabled.value ? 'true' : 'false');
isConfigured.value = true;
hasChanges.value = false;
showMessage('✅ Configuración guardada', 'success');
} catch (error) {
showMessage(`❌ Error: ${error}`, 'error');
}
}
async function testConnection() {
if (!apiKey.value) return;
testing.value = true;
showMessage('🔍 Probando conexión...', 'info');
try {
const result = await invoke<string[]>('get_gemini_completion', {
text: 'function hello() {\n ',
cursorPosition: 21,
language: 'javascript',
filePath: 'test.js',
apiKey: apiKey.value,
model: selectedModel.value,
agentMode: false // Siempre desactivar thinking en el test para que sea rápido
});
if (result && result.length > 0) {
showMessage(`✅ Funciona! Sugerencia: "${result[0].substring(0, 30)}..."`, 'success');
} else {
showMessage('⚠️ Sin respuesta. Verifica tu API key o aumenta max_output_tokens', 'error');
}
} catch (error) {
showMessage(`❌ Error: ${error}`, 'error');
} finally {
testing.value = false;
}
}
function showMessage(msg: string, type: 'success' | 'error' | 'info') {
message.value = msg;
messageType.value = type;
if (type === 'success') setTimeout(() => message.value = '', 5000);
}
</script>
<style scoped>
.gemini-settings { padding: 30px; max-width: 700px; margin: 0 auto; color: #e0e0e0; background: #1e1e1e; min-height: 100vh; }
.settings-header { margin-bottom: 30px; border-bottom: 2px solid rgba(66, 133, 244, 0.3); padding-bottom: 15px; }
.settings-header h3 { margin: 0 0 8px 0; font-size: 28px; color: #fff; font-weight: 600; background: linear-gradient(135deg, #4285f4 0%, #34a853 50%, #fbbc04 75%, #ea4335 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.subtitle { margin: 0 0 15px 0; color: #b0b0b0; font-size: 15px; }
.info-box { background: rgba(66, 133, 244, 0.1); border: 2px solid rgba(66, 133, 244, 0.3); border-radius: 8px; padding: 16px; margin-top: 15px; }
.info-box p { margin: 0 0 10px 0; color: #fff; font-size: 14px; }
.info-box ul { margin: 0; padding-left: 20px; list-style: none; }
.info-box li { margin: 8px 0; color: #e0e0e0; font-size: 13px; line-height: 1.6; position: relative; padding-left: 8px; }
.info-box li::before { content: "•"; position: absolute; left: -12px; color: #4285f4; font-weight: bold; }
.info-box kbd { background: #2d2d30; padding: 2px 6px; border-radius: 4px; border: 1px solid #3e3e42; font-family: monospace; font-size: 12px; color: #4285f4; }
.settings-content { display: flex; flex-direction: column; gap: 20px; }
.status-badge { display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 20px; font-size: 14px; font-weight: 600; width: fit-content; border: 2px solid; }
.status-badge.active { background: rgba(52, 168, 83, 0.25); color: #34a853; border-color: rgba(52, 168, 83, 0.5); }
.status-badge.inactive { background: rgba(158, 158, 158, 0.15); color: #ccc; border-color: rgba(158, 158, 158, 0.3); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.form-group { display: flex; flex-direction: column; gap: 8px; }
.form-group label { font-size: 14px; font-weight: 600; color: #fff; margin-bottom: 8px; }
.checkbox-label { display: flex; align-items: flex-start; gap: 12px; cursor: pointer; padding: 12px; background: #2d2d30; border-radius: 8px; border: 2px solid #3e3e42; transition: all 0.2s; }
.checkbox-label:hover { border-color: #4285f4; background: #353538; }
.checkbox-label.warning-label { border-color: #fbbc04; background: rgba(251, 188, 4, 0.05); }
.checkbox-label.warning-label:hover { border-color: #ffc928; background: rgba(251, 188, 4, 0.1); }
.checkbox-label input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; accent-color: #4285f4; margin-top: 2px; }
.checkbox-text { display: flex; flex-direction: column; gap: 4px; flex: 1; }
.checkbox-text strong { color: #fff; font-size: 14px; }
.checkbox-text small { color: #b0b0b0; font-size: 12px; line-height: 1.4; }
.checkbox-text .warning-text { color: #fbbc04; font-weight: 500; }
.checkbox-text .warning-text::before { content: "⚠️ "; }
.model-select, .api-key-input { padding: 12px 16px; background: #2d2d30; color: #e0e0e0; border: 2px solid #3e3e42; border-radius: 6px; font-size: 14px; transition: all 0.2s; }
.model-select:focus, .api-key-input:focus { outline: none; border-color: #4285f4; box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1); }
.input-wrapper { display: flex; gap: 10px; }
.api-key-input { flex: 1; font-family: 'Consolas', monospace; }
.toggle-visibility-btn { padding: 12px 16px; background: #3e3e42; color: #e0e0e0; border: 2px solid #3e3e42; border-radius: 6px; cursor: pointer; font-size: 18px; transition: all 0.2s; }
.toggle-visibility-btn:hover { background: #4e4e52; border-color: #4285f4; transform: scale(1.05); }
.helper-text { font-size: 13px; color: #b0b0b0; }
.link { color: #4285f4; text-decoration: none; font-weight: 500; }
.link:hover { color: #5a9dff; text-decoration: underline; }
.actions { display: flex; gap: 12px; margin-top: 10px; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: linear-gradient(135deg, #4285f4 0%, #34a853 100%); color: white; }
.btn-primary:hover:not(:disabled) { background: linear-gradient(135deg, #5a9dff 0%, #46ba65 100%); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4); }
.btn-test { background: #2d2d30; color: #e0e0e0; border: 2px solid #4285f4; }
.btn-test:hover:not(:disabled) { background: #3e3e42; transform: translateY(-2px); }
.message { padding: 14px 18px; border-radius: 8px; font-size: 14px; font-weight: 500; border: 2px solid; animation: slideIn 0.3s ease; }
.message.success { background: rgba(52, 168, 83, 0.2); color: #34a853; border-color: rgba(52, 168, 83, 0.5); }
.message.error { background: rgba(234, 67, 53, 0.2); color: #ea4335; border-color: rgba(234, 67, 53, 0.5); }
.message.info { background: rgba(66, 133, 244, 0.2); color: #4285f4; border-color: rgba(66, 133, 244, 0.5); }
@keyframes slideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
/* Responsive Design */
@media (max-width: 768px) {
.gemini-settings { padding: 20px; max-width: 100%; }
.settings-header h3 { font-size: 24px; }
.subtitle { font-size: 14px; }
.info-box { padding: 12px; }
.info-box p { font-size: 13px; }
.info-box li { font-size: 12px; }
.actions { flex-direction: column; }
.btn { width: 100%; }
.input-wrapper { flex-direction: column; }
.toggle-visibility-btn { width: 100%; }
}
@media (min-width: 1920px) {
.gemini-settings { max-width: 900px; padding: 40px; }
.settings-header h3 { font-size: 32px; }
.subtitle { font-size: 17px; }
}
</style>

View File

@@ -13,12 +13,31 @@
</div>
</div>
<div class="editor-content" ref="editorContainer"></div>
<!-- Widget de acciones de IA -->
<Transition name="fade">
<div v-if="showAIActions" class="ai-actions-widget" :style="aiWidgetPosition">
<button @click="handleAIAction('fix')" class="ai-action-btn fix-btn" title="Corregir código">
🔧 Fix
</button>
<button @click="handleAIAction('explain')" class="ai-action-btn explain-btn" title="Explicar código">
💡 Explain
</button>
<button @click="handleAIAction('refactor')" class="ai-action-btn refactor-btn" title="Refactorizar código">
Refactor
</button>
<button @click="handleAIAction('optimize')" class="ai-action-btn optimize-btn" title="Optimizar código">
Optimize
</button>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as monaco from 'monaco-editor';
import { invoke } from '@tauri-apps/api/core';
import type { FileInfo } from '../types/bot';
const props = defineProps<{
@@ -34,6 +53,9 @@ const emit = defineEmits<{
const editorContainer = ref<HTMLElement | null>(null);
let editor: monaco.editor.IStandaloneCodeEditor | null = null;
const hasChanges = ref(false);
const showAIActions = ref(false);
const aiWidgetPosition = ref({ top: '0px', left: '0px' });
const selectedText = ref('');
function getFileIcon(): string {
if (!props.fileInfo) return '📄';
@@ -43,6 +65,95 @@ function getFileIcon(): string {
return props.fileInfo.eventType === 'extra' ? '✨' : '🎯';
}
async function handleAIAction(action: 'fix' | 'explain' | 'refactor' | 'optimize') {
console.log('🎯 Acción de IA:', action);
if (!editor || !selectedText.value) {
console.log('⚠️ No hay editor o texto seleccionado');
return;
}
const apiKey = localStorage.getItem('gemini_api_key');
const model = localStorage.getItem('gemini_model') || 'gemini-2.5-flash';
if (!apiKey) {
alert('⚠️ Configura tu API key de Gemini primero en la sección "✨ Gemini IA"');
return;
}
const selection = editor.getSelection();
if (!selection) {
console.log('⚠️ No hay selección válida');
return;
}
console.log('📝 Texto seleccionado:', selectedText.value.substring(0, 50) + '...');
const prompts = {
fix: `Fix any errors or bugs in this code. Return ONLY the corrected code, no explanations:\n\n${selectedText.value}`,
explain: `Explain what this code does in Spanish. Be concise and clear:\n\n${selectedText.value}`,
refactor: `Refactor this code to make it cleaner and more maintainable. Return ONLY the refactored code:\n\n${selectedText.value}`,
optimize: `Optimize this code for better performance. Return ONLY the optimized code:\n\n${selectedText.value}`
};
try {
showAIActions.value = false;
console.log('🚀 Llamando a Gemini con ask_gemini...');
console.log('📝 Prompt:', prompts[action].substring(0, 100) + '...');
const result = await invoke<string>('ask_gemini', {
prompt: prompts[action],
apiKey,
model,
useThinking: true // Usar thinking para mejores resultados
});
console.log('✅ Respuesta recibida:', result.substring(0, 100) + '...');
if (result && result.length > 0) {
if (action === 'explain') {
// Mostrar explicación en un mensaje
alert(`💡 Explicación:\n\n${result}`);
} else {
// Reemplazar código seleccionado
const edit = {
range: selection,
text: result
};
editor.executeEdits('ai-action', [edit]);
hasChanges.value = true;
console.log('✅ Código reemplazado');
}
} else {
console.log('⚠️ No se recibió respuesta');
alert('⚠️ No se pudo obtener una respuesta de Gemini. Intenta de nuevo.');
}
} catch (error) {
console.error('❌ Error en acción de IA:', error);
alert(`❌ Error: ${error}`);
}
}
function getLanguageFromFile(): string {
const ext = props.fileInfo?.path?.split('.').pop() || 'txt';
const langMap: Record<string, string> = {
'ts': 'typescript',
'js': 'javascript',
'json': 'json',
'md': 'markdown',
'py': 'python',
'rs': 'rust',
'go': 'go',
'java': 'java',
'cpp': 'cpp',
'c': 'c',
'html': 'html',
'css': 'css',
'vue': 'vue'
};
return langMap[ext] || 'text';
}
function saveFile() {
if (editor && hasChanges.value) {
const content = editor.getValue();
@@ -668,8 +779,9 @@ onMounted(() => {
// Sugerencias inline (como GitHub Copilot)
inlineSuggest: {
enabled: true,
mode: 'prefix',
showToolbar: 'onHover'
mode: 'subwordSmart',
showToolbar: 'always',
suppressSuggestions: false
},
// Peekable definition/references
@@ -1499,12 +1611,168 @@ onMounted(() => {
}
});
// ============================================
// CODEIUM INLINE COMPLETION PROVIDER
// ============================================
let lastCompletionTimeout: number | null = null;
// Solo registrar si el usuario tiene activadas las sugerencias inline
const inlineSuggestionsEnabled = localStorage.getItem('gemini_inline_suggestions') === 'true';
if (inlineSuggestionsEnabled) {
console.log('✅ Sugerencias inline de Gemini habilitadas');
// Registrar proveedor de completación inline (estilo Copilot)
monaco.languages.registerInlineCompletionsProvider('typescript', {
provideInlineCompletions: async (model, position) => {
// Cancelar solicitud anterior si existe
if (lastCompletionTimeout) {
clearTimeout(lastCompletionTimeout);
}
return new Promise((resolve) => {
lastCompletionTimeout = window.setTimeout(async () => {
try {
const text = model.getValue();
const offset = model.getOffsetAt(position);
const lineContent = model.getLineContent(position.lineNumber);
// Solo sugerir si hay contenido en la línea actual o si es inicio de línea
if (lineContent.trim().length === 0 && position.column > 1) {
resolve({ items: [] });
return;
}
console.log('🤖 Solicitando sugerencia de Gemini...');
// Llamar al backend para obtener sugerencias
const suggestions = await invoke<string[]>('get_gemini_completion', {
text,
cursorPosition: offset,
language: 'typescript',
filePath: props.fileInfo?.path || 'untitled.ts',
apiKey: localStorage.getItem('gemini_api_key') || '',
model: localStorage.getItem('gemini_model') || 'gemini-2.5-flash',
agentMode: localStorage.getItem('gemini_agent_mode') === 'true'
});
if (suggestions && suggestions.length > 0) {
console.log('✅ Sugerencias recibidas:', suggestions);
const items = suggestions.map((suggestion: string) => ({
insertText: suggestion,
range: {
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column
}
}));
console.log('📤 Items a mostrar:', items);
resolve({ items });
} else {
console.log('⚠️ No se recibieron sugerencias');
resolve({ items: [] });
}
} catch (error) {
console.error('❌ Error obteniendo sugerencia:', error);
resolve({ items: [] });
}
}, 500); // Debounce de 500ms
});
},
// Método requerido por Monaco para liberar recursos
disposeInlineCompletions: () => {}
});
// También para JavaScript
monaco.languages.registerInlineCompletionsProvider('javascript', {
provideInlineCompletions: async (model, position) => {
if (lastCompletionTimeout) {
clearTimeout(lastCompletionTimeout);
}
return new Promise((resolve) => {
lastCompletionTimeout = window.setTimeout(async () => {
try {
const text = model.getValue();
const offset = model.getOffsetAt(position);
const suggestions = await invoke<string[]>('get_gemini_completion', {
text,
cursorPosition: offset,
language: 'javascript',
filePath: props.fileInfo?.path || 'untitled.js',
apiKey: localStorage.getItem('gemini_api_key') || '',
model: localStorage.getItem('gemini_model') || 'gemini-2.5-flash',
agentMode: localStorage.getItem('gemini_agent_mode') === 'true'
});
if (suggestions && suggestions.length > 0) {
const items = suggestions.map((suggestion: string) => ({
insertText: suggestion,
range: {
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: position.column
}
}));
resolve({ items });
} else {
resolve({ items: [] });
}
} catch (error) {
console.error('❌ Error obteniendo sugerencia:', error);
resolve({ items: [] });
}
}, 500);
});
},
disposeInlineCompletions: () => {}
});
} else {
console.log(' Sugerencias inline de Gemini deshabilitadas. Usa los botones de acción (Fix, Explain, etc.) para invocar la IA.');
}
console.log('🤖 Gemini inline completion provider registrado');
// Detectar cambios
editor.onDidChangeModelContent(() => {
hasChanges.value = true;
emit('change', editor!.getValue());
});
// Detectar selección de texto para mostrar widget de IA
editor.onDidChangeCursorSelection(() => {
const selection = editor!.getSelection();
if (selection && !selection.isEmpty()) {
const text = editor!.getModel()!.getValueInRange(selection);
if (text.trim().length > 0) {
selectedText.value = text;
console.log('✅ Texto seleccionado para IA:', text.substring(0, 50) + '...');
// Posicionar widget relativo al editor
const layoutInfo = editor!.getLayoutInfo();
const selectionStart = editor!.getScrolledVisiblePosition(selection.getStartPosition());
if (selectionStart) {
aiWidgetPosition.value = {
top: `${Math.max(0, selectionStart.top - 50)}px`,
left: `${Math.min(selectionStart.left, layoutInfo.width - 400)}px`
};
showAIActions.value = true;
console.log('🔧 Widget mostrado');
}
}
} else {
showAIActions.value = false;
selectedText.value = '';
}
});
// Atajos de teclado
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
saveFile();
@@ -1548,6 +1816,7 @@ onUnmounted(() => {
flex-direction: column;
height: 100%;
background-color: #1e1e1e;
position: relative;
}
.editor-header {
@@ -1614,6 +1883,77 @@ onUnmounted(() => {
overflow: hidden;
}
/* Widget de acciones de IA */
.ai-actions-widget {
position: absolute;
display: flex;
gap: 6px;
padding: 8px;
background: #252526;
border: 2px solid #007acc;
border-radius: 8px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.6);
z-index: 999999;
pointer-events: all;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.ai-action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.2s;
color: #fff;
white-space: nowrap;
}
.ai-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.fix-btn {
background: linear-gradient(135deg, #f48771 0%, #e74c3c 100%);
}
.fix-btn:hover {
background: linear-gradient(135deg, #ff9a85 0%, #ff5c4d 100%);
}
.explain-btn {
background: linear-gradient(135deg, #4fc3f7 0%, #2196f3 100%);
}
.explain-btn:hover {
background: linear-gradient(135deg, #6dd5f9 0%, #42a5f5 100%);
}
.refactor-btn {
background: linear-gradient(135deg, #66bb6a 0%, #4caf50 100%);
}
.refactor-btn:hover {
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
}
.optimize-btn {
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
}
.optimize-btn:hover {
background: linear-gradient(135deg, #ffca64 0%, #ffa726 100%);
}
/* Estilos para mejor visualización de errores y warnings */
:deep(.monaco-editor .squiggly-error) {
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23f48771'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left;

View File

@@ -36,6 +36,9 @@
<button @click="emit('toggle-env-manager')" class="action-btn env-manager">
🔐 Variables ENV
</button>
<button @click="emit('toggle-gemini-settings')" class="action-btn gemini">
Gemini IA
</button>
<button @click="emit('refresh')" class="action-btn secondary">
🔄 Refrescar
</button>
@@ -187,6 +190,7 @@ const emit = defineEmits<{
'toggle-dev-ultra': [];
'toggle-database': [];
'toggle-env-manager': [];
'toggle-gemini-settings': [];
'notify': [message: string, type: 'success' | 'error' | 'info'];
}>();
@@ -355,6 +359,19 @@ function truncatePath(path: string): string {
transform: translateY(-1px);
}
.action-btn.gemini {
background: linear-gradient(135deg, #4285f4 0%, #34a853 50%, #fbbc04 75%, #ea4335 100%);
color: white;
font-weight: 600;
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.3);
}
.action-btn.gemini:hover {
background: linear-gradient(135deg, #5a9dff 0%, #46ba65 50%, #ffc825 75%, #f55a4e 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.5);
}
.files-section {
flex: 1;
overflow-y: auto;