diff --git a/client/package-lock.json b/client/package-lock.json index 2af86b9..3b240a8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -20,6 +20,7 @@ "clsx": "^2.1.1", "dagre": "^0.8.5", "lucide-react": "^0.562.0", + "prismjs": "^1.30.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-redux": "^9.2.0", @@ -2014,9 +2015,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -2027,9 +2028,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -2040,9 +2041,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -2053,9 +2054,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -2066,9 +2067,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -2079,9 +2080,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -2092,9 +2093,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], @@ -2105,9 +2106,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], @@ -2118,9 +2119,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], @@ -2131,9 +2132,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], @@ -2144,9 +2145,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], @@ -2157,9 +2158,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], @@ -2170,9 +2171,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], @@ -2183,9 +2184,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], @@ -2196,9 +2197,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], @@ -2209,9 +2210,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], @@ -2222,9 +2223,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], @@ -2235,9 +2236,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], @@ -2248,9 +2249,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], @@ -2261,9 +2262,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], @@ -2274,9 +2275,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -2287,9 +2288,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -2300,9 +2301,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -2313,9 +2314,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -2326,9 +2327,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -2339,50 +2340,50 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.46.0.tgz", - "integrity": "sha512-WB1gBT9G13V02ekZ6NpUhoI1aGHV2eNfjEPthkU2bGBvFpQKnstwzjg7waIRGR7cu+YSW2Q6UI6aQLgBeOPD1g==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.47.0.tgz", + "integrity": "sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw==", "license": "MIT", "dependencies": { - "@sentry/core": "10.46.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.46.0.tgz", - "integrity": "sha512-c4pI/z9nZCQXe9GYEw/hE/YTY9AxGBp8/wgKI+T8zylrN35SGHaXv63szzE1WbI8lacBY8lBF7rstq9bQVCaHw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.47.0.tgz", + "integrity": "sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg==", "license": "MIT", "dependencies": { - "@sentry/core": "10.46.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.46.0.tgz", - "integrity": "sha512-JBsWeXG6bRbxBFK8GzWymWGOB9QE7Kl57BeF3jzgdHTuHSWZ2mRnAmb1K05T4LU+gVygk6yW0KmdC8Py9Qzg9A==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.47.0.tgz", + "integrity": "sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.46.0.tgz", - "integrity": "sha512-ub314MWUsekVCuoH0/HJbbimlI24SkV745UW2pj9xRbxOAEf1wjkmIzxKrMDbTgJGuEunug02XZVdJFJUzOcDw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.47.0.tgz", + "integrity": "sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/replay": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -2416,38 +2417,38 @@ } }, "node_modules/@sentry/browser": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.46.0.tgz", - "integrity": "sha512-80DmGlTk5Z2/OxVOzLNxwolMyouuAYKqG8KUcoyintZqHbF6kO1RulI610HmyUt3OagKeBCqt9S7w0VIfCRL+Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.47.0.tgz", + "integrity": "sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.46.0", - "@sentry-internal/feedback": "10.46.0", - "@sentry-internal/replay": "10.46.0", - "@sentry-internal/replay-canvas": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry-internal/feedback": "10.47.0", + "@sentry-internal/replay": "10.47.0", + "@sentry-internal/replay-canvas": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/core": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.46.0.tgz", - "integrity": "sha512-N3fj4zqBQOhXliS1Ne9euqIKuciHCGOJfPGQLwBoW9DNz03jF+NB8+dUKtrJ79YLoftjVgf8nbgwtADK7NR+2Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.47.0.tgz", + "integrity": "sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.46.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.46.0.tgz", - "integrity": "sha512-Rb1S+9OuUPVwsz7GWnQ6Kgf3azbsseUymIegg3JZHNcW/fM1nPpaljzTBnuineia113DH0pgMBcdrrZDLaosFQ==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.47.0.tgz", + "integrity": "sha512-ZtJV6xxF8jUVE9e3YQUG3Do0XapG1GjniyLyqMPgN6cNvs/HaRJODf7m60By+VGqcl5XArEjEPTvx8CdPUXDfA==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.46.0", - "@sentry/core": "10.46.0" + "@sentry/browser": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -3225,9 +3226,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", - "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3249,9 +3250,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -3270,11 +3271,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3307,9 +3308,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", "dev": true, "funding": [ { @@ -3632,9 +3633,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.328", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz", - "integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==", + "version": "1.5.329", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", + "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", "dev": true, "license": "ISC" }, @@ -4711,9 +4712,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.0.tgz", + "integrity": "sha512-l1mfj2atMqndAHI3ls7XqPxEjV2J9ZkcNyHpoZA3r2T1LLwDB69jgkMWh71YKwhBbK0G2f4WSn05ahmQXVxupA==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -4973,6 +4974,15 @@ "node": ">= 0.8.0" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -5207,9 +5217,9 @@ } }, "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -5222,31 +5232,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, diff --git a/client/package.json b/client/package.json index 070296d..b723fe5 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "clsx": "^2.1.1", "dagre": "^0.8.5", "lucide-react": "^0.562.0", + "prismjs": "^1.30.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-redux": "^9.2.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index b4c6d51..4615553 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,7 +10,9 @@ import Layout from '@/components/layout/Layout'; import { LandingPage, LoginPage, SignupPage } from '@/features/auth'; import { DashboardPage } from '@/features/dashboard'; -import { AnalyzePage, GraphPage } from '@/features/graph'; +import { UploadRepoPage, GraphPage } from '@/features/graph'; +import { AnalyzeFilePage, AnalyzePage } from '@/features/analyze'; +import { AskPage } from '@/features/ai'; function RootRedirect() { const { isAuthenticated, loading } = useAuth(); @@ -39,8 +41,13 @@ function AppRoutes() { }> }> } /> + } /> } /> + } /> + } /> + } /> } /> + } /> diff --git a/client/src/app/store.js b/client/src/app/store.js index 040ef79..b022037 100644 --- a/client/src/app/store.js +++ b/client/src/app/store.js @@ -3,6 +3,7 @@ import themeReducer from '@/features/theme/slices/themeSlice'; import graphReducer from '@/features/graph/slices/graphSlice'; import dashboardReducer from '@/features/dashboard/slices/dashboardSlice'; import aiReducer from '@/features/ai/slices/aiSlice'; +import analyzeReducer from '@/features/analyze/slices/analyzeSlice'; export const store = configureStore({ reducer: { @@ -10,6 +11,7 @@ export const store = configureStore({ graph: graphReducer, dashboard: dashboardReducer, ai: aiReducer, + analyze: analyzeReducer, }, devTools: import.meta.env.DEV, }); diff --git a/client/src/components/layout/Sidebar.jsx b/client/src/components/layout/Sidebar.jsx index 6787576..c560113 100644 --- a/client/src/components/layout/Sidebar.jsx +++ b/client/src/components/layout/Sidebar.jsx @@ -7,6 +7,9 @@ import { Share2, ChevronLeft, ChevronRight, + UploadIcon, + GitGraphIcon, + MessageSquare, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -16,16 +19,26 @@ const NAV_ITEMS = [ icon: , label: 'Dashboard', }, + { + to: '/upload-repo', + icon: , + label: 'Upload Repo', + }, { to: '/analyze', - icon: , + icon: , label: 'Analyze', }, { to: '/graph', - icon: , + icon: , label: 'Graph', }, + { + to: '/ask', + icon: , + label: 'Ask', + }, ]; export default function Sidebar({ diff --git a/client/src/components/ui/button.jsx b/client/src/components/ui/button.jsx index 0f180cf..f4fc2d9 100644 --- a/client/src/components/ui/button.jsx +++ b/client/src/components/ui/button.jsx @@ -4,7 +4,7 @@ import { cva } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[transform,background-color,border-color,text-color,box-shadow] duration-200 ease-[var(--ease-out)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.97]", { variants: { variant: { @@ -17,6 +17,7 @@ const buttonVariants = cva( "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", + neumo: "bg-background shadow-neu-flat hover:shadow-neu-inset text-foreground transition-all duration-300", }, size: { default: "h-10 px-4 py-2", diff --git a/client/src/components/ui/input.jsx b/client/src/components/ui/input.jsx index 9db2b49..6b8c2b8 100644 --- a/client/src/components/ui/input.jsx +++ b/client/src/components/ui/input.jsx @@ -6,7 +6,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => { { + if (!jobId || !nodeId || isLoadingRefactor) return; + + setIsLoadingRefactor(true); + setRefactorError(''); + + try { + const result = await aiService.suggestRefactor({ + jobId, + filePath: nodeId, + }); + + setRefactorSuggestion(result); + } catch (error) { + setRefactorSuggestion(null); + setRefactorError(error?.response?.data?.error || error?.message || 'Failed to load suggestions.'); + } finally { + setIsLoadingRefactor(false); + } + }; + return ( -
+
{nodeId} @@ -124,6 +150,66 @@ export default function AiPanel({ nodeId, graph, onClose }) { )}
+
+
+

+ Refactor Suggestions +

+ +
+ + {isLoadingRefactor && ( +
+ + Evaluating architecture risk... +
+ )} + + {refactorError && ( +

+ {refactorError} +

+ )} + + {refactorSuggestion && !isLoadingRefactor && !refactorError && ( +
+

+ Priority: {refactorSuggestion.priority || 'medium'} + {' '}ยท Effort: {refactorSuggestion.estimatedEffort || 'unknown'} +

+ + {refactorSuggestion.concerns?.length > 0 && ( +
+

Concerns

+
    + {refactorSuggestion.concerns.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} + + {refactorSuggestion.suggestions?.length > 0 && ( +
+

Suggestions

+
    + {refactorSuggestion.suggestions.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} +
+ )} +
+ {declarations.length > 0 && (

@@ -154,7 +240,7 @@ export default function AiPanel({ nodeId, graph, onClose }) { {impactedFiles.length > 0 && (

-
    +
      {impactedFiles.map((file) => (
    • {file}
    • ))} @@ -166,7 +252,7 @@ export default function AiPanel({ nodeId, graph, onClose }) { {deps.length > 0 && (

      Imports ({deps.length})

      -
        +
          {deps.map((dep) => (
        • {dep}
        • ))} @@ -177,7 +263,7 @@ export default function AiPanel({ nodeId, graph, onClose }) { {usedBy.length > 0 && (

          Used By ({usedBy.length})

          -
            +
              {usedBy.map((file) => (
            • {file}
            • ))} diff --git a/client/src/features/ai/components/QueryBar.jsx b/client/src/features/ai/components/QueryBar.jsx index a4ce3b0..71783be 100644 --- a/client/src/features/ai/components/QueryBar.jsx +++ b/client/src/features/ai/components/QueryBar.jsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Search, X, CheckCircle, AlertCircle } from 'lucide-react'; +import { Search, X, CheckCircle, AlertCircle, Loader2 } from 'lucide-react'; import { queryGraph, resetAiState, selectAiQueryState } from '../slices/aiSlice'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -53,10 +53,10 @@ export default function QueryBar({ jobId }) {
              {/* Minimalist Query Input */}
              @@ -87,17 +87,17 @@ export default function QueryBar({ jobId }) {
              @@ -105,10 +105,10 @@ export default function QueryBar({ jobId }) { {/* Results Display */} {(hasResult || hasError) && (
              diff --git a/client/src/features/ai/components/QueryHistory.jsx b/client/src/features/ai/components/QueryHistory.jsx index 2abc625..712195b 100644 --- a/client/src/features/ai/components/QueryHistory.jsx +++ b/client/src/features/ai/components/QueryHistory.jsx @@ -94,25 +94,25 @@ export default function QueryHistory({ jobId }) { if (!jobId) return null; return ( -
              +
              @@ -128,7 +128,7 @@ export default function QueryHistory({ jobId }) { )} {!error && visibleQueries.length > 0 && ( -
                +
                  {visibleQueries.map((queryItem) => (
                • diff --git a/client/src/features/ai/index.js b/client/src/features/ai/index.js index 65ebe2e..c26a49b 100644 --- a/client/src/features/ai/index.js +++ b/client/src/features/ai/index.js @@ -15,3 +15,4 @@ export { aiService } from './services/aiService'; export { default as QueryBar } from './components/QueryBar'; export { default as QueryHistory } from './components/QueryHistory'; export { default as AiPanel } from './components/AiPanel'; +export { default as AskPage } from './pages/AskPage'; diff --git a/client/src/features/ai/pages/AskPage.jsx b/client/src/features/ai/pages/AskPage.jsx new file mode 100644 index 0000000..db526ae --- /dev/null +++ b/client/src/features/ai/pages/AskPage.jsx @@ -0,0 +1,100 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { Link, useSearchParams } from 'react-router-dom'; +import { AlertCircle, MessageSquareText, Network, Sparkles } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { QueryBar, QueryHistory } from '@/features/ai'; +import { selectGraphData } from '@/features/graph'; + +export default function AskPage() { + const [searchParams] = useSearchParams(); + const graphData = useSelector(selectGraphData); + + const activeJobId = useMemo(() => { + const urlJobId = String(searchParams.get('jobId') || '').trim(); + if (urlJobId) return urlJobId; + return graphData?.jobId || null; + }, [graphData?.jobId, searchParams]); + + return ( +
                  +
                  +
                  +
                  +

                  + + Ask Workspace AI +

                  +

                  Ask questions about your codebase

                  +

                  + Use natural language to inspect architecture, dependencies, and design trade-offs. + Responses are scoped to your active analysis and saved in query history. +

                  +
                  + +
                  + + + +
                  +
                  +
                  + + {!activeJobId ? ( +
                  +
                  + +
                  +

                  + No active analysis found. Load a saved graph or run a new analysis first to ask + context-aware questions. +

                  +
                  + + + + + + +
                  +
                  +
                  +
                  + ) : ( +
                  +
                  +
                  +
                  +

                  + Query +

                  +

                  + Active analysis: {activeJobId} +

                  +
                  + + + Ready + +
                  + + +
                  + + +
                  + )} +
                  + ); +} diff --git a/client/src/features/ai/services/aiService.js b/client/src/features/ai/services/aiService.js index df616d6..243fd5c 100644 --- a/client/src/features/ai/services/aiService.js +++ b/client/src/features/ai/services/aiService.js @@ -107,6 +107,52 @@ export const aiService = { return data; }, + async analyzeSnippetImpact({ jobId, filePath, snippet, lineStart, lineEnd, signal }) { + const normalizedJobId = normalizeText(jobId); + const normalizedFilePath = normalizeText(filePath); + const normalizedSnippet = String(snippet || '').trim(); + + if (!normalizedJobId || !normalizedFilePath || !normalizedSnippet) { + throw new Error('analyzeSnippetImpact requires jobId, filePath, and snippet.'); + } + + const payload = { + jobId: normalizedJobId, + filePath: normalizedFilePath, + snippet: normalizedSnippet, + }; + + if (Number.isInteger(lineStart) && lineStart > 0) payload.lineStart = lineStart; + if (Number.isInteger(lineEnd) && lineEnd > 0) payload.lineEnd = lineEnd; + + const { data } = await aiClient.post('/api/ai/snippet-impact', payload, { + signal, + }); + return data; + }, + + async suggestRefactor({ jobId, filePath }) { + const normalizedJobId = normalizeText(jobId); + const normalizedFilePath = normalizeText(filePath); + + if (!normalizedJobId || !normalizedFilePath) { + throw new Error('suggestRefactor requires jobId and filePath.'); + } + + const { data } = await aiClient.post('/api/ai/suggest-refactor', { + jobId: normalizedJobId, + filePath: normalizedFilePath, + }); + + return { + filePath: normalizeText(data?.filePath) || normalizedFilePath, + concerns: Array.isArray(data?.concerns) ? data.concerns : [], + suggestions: Array.isArray(data?.suggestions) ? data.suggestions : [], + priority: normalizeText(data?.priority) || 'medium', + estimatedEffort: normalizeText(data?.estimatedEffort) || 'unknown', + }; + }, + async streamExplain({ question, jobId, onChunk, onDone, onError, signal } = {}) { const normalizedQuestion = normalizeText(question); const normalizedJobId = normalizeText(jobId); diff --git a/client/src/features/analyze/index.js b/client/src/features/analyze/index.js new file mode 100644 index 0000000..369a0dd --- /dev/null +++ b/client/src/features/analyze/index.js @@ -0,0 +1,17 @@ +export { default as AnalyzePage } from './pages/AnalyzePage'; +export { default as AnalyzeFilePage } from './pages/AnalyzeFilePage'; + +export { + fetchRepositoryFile, + fetchDirectoryContents, + fetchRepositoryStructure, + saveRepositoryFile, + setSelectedAnalyzeRepository, + selectAnalyzeContents, + selectAnalyzeFile, + selectAnalyzeSelectedRepository, + selectAnalyzeStructure, + default as analyzeReducer, +} from './slices/analyzeSlice'; + +export { analyzeService } from './services/analyzeService'; \ No newline at end of file diff --git a/client/src/features/analyze/pages/AnalyzeFilePage.jsx b/client/src/features/analyze/pages/AnalyzeFilePage.jsx new file mode 100644 index 0000000..ab54c59 --- /dev/null +++ b/client/src/features/analyze/pages/AnalyzeFilePage.jsx @@ -0,0 +1,1140 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import Prism from 'prismjs'; +import 'prismjs/components/prism-markup'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-jsx'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-tsx'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-json'; +import 'prismjs/components/prism-bash'; +import 'prismjs/components/prism-python'; +import 'prismjs/components/prism-markdown'; +import 'prismjs/components/prism-yaml'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { + ArrowLeft, + AlertTriangle, + Check, + Edit3, + ExternalLink, + GitBranch, + Loader2, + Pin, + Save, + X, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + fetchRepositoryFile, + fetchRepositoryStructure, + saveRepositoryFile, + selectAnalyzeFile, + selectAnalyzeSelectedRepository, + selectAnalyzeStructure, +} from '../slices/analyzeSlice'; +import { AiPanel } from '@/features/ai'; +import { loadSavedGraph, selectGraphData } from '@/features/graph'; +import { aiService } from '@/features/ai/services/aiService'; + +function detectPrismLanguage(filePath = '') { + const normalized = String(filePath).toLowerCase(); + + if (normalized.endsWith('.ts')) return 'typescript'; + if (normalized.endsWith('.tsx')) return 'tsx'; + if (normalized.endsWith('.js')) return 'javascript'; + if (normalized.endsWith('.jsx')) return 'jsx'; + if (normalized.endsWith('.json')) return 'json'; + if (normalized.endsWith('.css')) return 'css'; + if (normalized.endsWith('.html')) return 'markup'; + if (normalized.endsWith('.md')) return 'markdown'; + if (normalized.endsWith('.yml') || normalized.endsWith('.yaml')) return 'yaml'; + if (normalized.endsWith('.py')) return 'python'; + if (normalized.endsWith('.sh')) return 'bash'; + + return 'clike'; +} + +const SNIPPET_MIN_CHARS = 10; +const SNIPPET_MAX_AUTO_CHARS = 1200; + +function isMeaningfulSnippet(snippet = '') { + const normalized = String(snippet || '').trim(); + if (!normalized) return false; + if (normalized.length < SNIPPET_MIN_CHARS) return false; + + const withoutComments = normalized + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|\n)\s*\/\/.*$/gm, '') + .trim(); + + if (!withoutComments) return false; + if (/^[{}()[\];,\s]+$/.test(withoutComments)) return false; + + return true; +} + +function normalizeConfidenceScore(value) { + if (typeof value === 'number' && Number.isFinite(value)) { + return Math.max(0, Math.min(1, value)); + } + + const parsed = Number.parseFloat(String(value || '').replace('%', '').trim()); + if (Number.isNaN(parsed)) return null; + const score = parsed > 1 ? parsed / 100 : parsed; + return Math.max(0, Math.min(1, score)); +} + +function confidenceTone(score) { + if (typeof score !== 'number') return 'text-muted-foreground'; + if (score >= 0.85) return 'text-emerald-600'; + if (score >= 0.65) return 'text-amber-600'; + return 'text-rose-600'; +} + +export default function AnalyzeFilePage() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const params = useParams(); + const [searchParams] = useSearchParams(); + + const selectedRepository = useSelector(selectAnalyzeSelectedRepository); + const structure = useSelector(selectAnalyzeStructure); + const fileState = useSelector(selectAnalyzeFile); + const graphData = useSelector(selectGraphData); + + const [editorValue, setEditorValue] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const [isAutoSnippetAnalyze, setIsAutoSnippetAnalyze] = useState(true); + const [snippetState, setSnippetState] = useState({ + status: 'idle', + error: '', + notice: '', + selectedSnippet: '', + lineStart: null, + lineEnd: null, + data: null, + }); + const [isSnippetDrawerOpen, setIsSnippetDrawerOpen] = useState(false); + const [isMobileSnippetSheetOpen, setIsMobileSnippetSheetOpen] = useState(false); + const [isSnippetPopoverPinned, setIsSnippetPopoverPinned] = useState(false); + const [snippetPopoverAnchor, setSnippetPopoverAnchor] = useState({ + x: 0, + y: 0, + visible: false, + }); + + const editorGutterRef = useRef(null); + const editorTextareaRef = useRef(null); + const viewerGutterRef = useRef(null); + const viewerCodeRef = useRef(null); + const snippetAbortRef = useRef(null); + const snippetDebounceRef = useRef(null); + const snippetPanelRef = useRef(null); + + const routeDirectory = useMemo(() => { + const raw = params.dir_name ? decodeURIComponent(params.dir_name) : ''; + return raw.trim(); + }, [params.dir_name]); + + const currentPath = useMemo(() => { + const queryPath = String(searchParams.get('path') || '').trim(); + if (queryPath) return queryPath; + return routeDirectory; + }, [routeDirectory, searchParams]); + + const selectedFilePath = useMemo(() => { + return String(searchParams.get('file') || '').trim(); + }, [searchParams]); + + useEffect(() => { + dispatch(fetchRepositoryStructure()); + }, [dispatch]); + + useEffect(() => { + if (!selectedFilePath) return; + dispatch(fetchRepositoryFile({ path: selectedFilePath })); + }, [dispatch, selectedFilePath]); + + const analysisJobId = selectedRepository?.jobId || selectedRepository?.latestJobId || graphData?.jobId || null; + + useEffect(() => { + if (!analysisJobId) return; + if (graphData?.jobId === analysisJobId) return; + + dispatch( + loadSavedGraph({ + jobId: analysisJobId, + rootDir: selectedRepository?.fullName || null, + }), + ); + }, [analysisJobId, dispatch, graphData?.jobId, selectedRepository?.fullName]); + + useEffect(() => { + const fileContent = fileState.data?.content; + if (typeof fileContent !== 'string') return; + setEditorValue(fileContent); + setIsEditing(false); + setSnippetState({ + status: 'idle', + error: '', + notice: '', + selectedSnippet: '', + lineStart: null, + lineEnd: null, + data: null, + }); + setSnippetPopoverAnchor((prev) => ({ ...prev, visible: false })); + }, [fileState.data?.content, fileState.data?.path]); + + useEffect(() => { + return () => { + if (snippetDebounceRef.current) { + clearTimeout(snippetDebounceRef.current); + } + if (snippetAbortRef.current) { + snippetAbortRef.current.abort(); + } + }; + }, []); + + const codeLanguage = useMemo( + () => detectPrismLanguage(fileState.data?.path || selectedFilePath), + [fileState.data?.path, selectedFilePath], + ); + + const highlightedContent = useMemo(() => { + const value = String(fileState.data?.content || ''); + const grammar = Prism.languages[codeLanguage] || Prism.languages.clike; + return Prism.highlight(value, grammar, codeLanguage); + }, [codeLanguage, fileState.data?.content]); + + const viewerLineCount = useMemo(() => { + const value = String(fileState.data?.content || ''); + return Math.max(1, value.split('\n').length); + }, [fileState.data?.content]); + + const editorLineCount = useMemo(() => { + return Math.max(1, String(editorValue || '').split('\n').length); + }, [editorValue]); + + const hasUnsavedChanges = + typeof fileState.data?.content === 'string' && + editorValue !== fileState.data.content; + + const handleSaveFile = async () => { + if (!fileState.data?.path || !fileState.data?.sha) return; + + await dispatch( + saveRepositoryFile({ + path: fileState.data.path, + content: editorValue, + sha: fileState.data.sha, + message: `Update ${fileState.data.path} via CodeGraph AI editor`, + }), + ); + + setIsEditing(false); + }; + + + + const backToExplorer = `/analyze/${encodeURIComponent(routeDirectory)}?path=${encodeURIComponent(currentPath)}`; + + const aiGraph = useMemo(() => { + const graphObject = graphData?.graph; + + if (graphObject && typeof graphObject === 'object' && !Array.isArray(graphObject)) { + return graphObject; + } + + const fallbackNodes = Array.isArray(graphData?.nodes) ? graphData.nodes : []; + return fallbackNodes.reduce((acc, node) => { + if (!node?.id) return acc; + acc[node.id] = { + deps: Array.isArray(node.deps) ? node.deps : [], + type: node.type || 'file', + summary: node.summary || null, + declarations: Array.isArray(node.declarations) ? node.declarations : [], + }; + return acc; + }, {}); + }, [graphData?.graph, graphData?.nodes]); + + const hasNodeInsights = Boolean(selectedFilePath && aiGraph?.[selectedFilePath]); + + const snippetConfidenceScore = useMemo(() => { + return normalizeConfidenceScore(snippetState.data?.confidenceScore); + }, [snippetState.data?.confidenceScore]); + + const quickSnippetSummary = useMemo(() => { + if (!snippetState.data?.whatItDoes) return ''; + const raw = String(snippetState.data.whatItDoes).replace(/\s+/g, ' ').trim(); + if (raw.length <= 140) return raw; + return `${raw.slice(0, 137)}...`; + }, [snippetState.data?.whatItDoes]); + + const snippetStatusMeta = useMemo(() => { + if (snippetState.status === 'loading') { + return { + label: 'Analyzing', + dotClass: 'bg-amber-500 animate-pulse', + }; + } + + if (snippetState.status === 'succeeded') { + return { + label: 'Ready', + dotClass: 'bg-emerald-500', + }; + } + + if (snippetState.status === 'failed') { + return { + label: 'Error', + dotClass: 'bg-rose-500', + }; + } + + return { + label: 'Idle', + dotClass: 'bg-slate-400', + }; + }, [snippetState.status]); + + const getLineNumberFromOffset = (value, offset) => { + const safeOffset = Math.max(0, Math.min(String(value || '').length, offset)); + const upToOffset = String(value || '').slice(0, safeOffset); + return upToOffset.split('\n').length; + }; + + const openSnippetDrawer = () => { + setIsSnippetDrawerOpen(true); + if (window.matchMedia('(max-width: 1279px)').matches) { + setIsMobileSnippetSheetOpen(true); + } + window.setTimeout(() => { + snippetPanelRef.current?.focus(); + }, 20); + }; + + const updateSnippetPopoverAnchor = ({ x, y, visible = true }) => { + setSnippetPopoverAnchor({ + x: Number.isFinite(x) ? Math.max(16, x) : 16, + y: Number.isFinite(y) ? Math.max(16, y) : 16, + visible, + }); + }; + + const triggerSnippetAnalysis = ({ + snippet, + lineStart, + lineEnd, + shouldAnalyze = true, + triggerSource = 'auto', + }) => { + const normalizedSnippet = String(snippet || '').trim(); + + const basePayload = { + selectedSnippet: normalizedSnippet, + lineStart: Number.isInteger(lineStart) ? lineStart : null, + lineEnd: Number.isInteger(lineEnd) ? lineEnd : null, + }; + + if (!shouldAnalyze) { + if (snippetDebounceRef.current) { + clearTimeout(snippetDebounceRef.current); + } + if (snippetAbortRef.current) { + snippetAbortRef.current.abort(); + } + + setSnippetState((prev) => ({ + ...prev, + status: 'idle', + error: '', + notice: normalizedSnippet ? 'Selection captured. Click Analyze Snippet to run.' : '', + ...basePayload, + })); + return; + } + + if (!normalizedSnippet) { + if (snippetAbortRef.current) { + snippetAbortRef.current.abort(); + } + + setSnippetState((prev) => ({ + ...prev, + status: 'idle', + error: '', + notice: '', + ...basePayload, + data: null, + })); + return; + } + + if (!isMeaningfulSnippet(normalizedSnippet)) { + setSnippetState((prev) => ({ + ...prev, + status: 'idle', + error: '', + notice: `Select at least ${SNIPPET_MIN_CHARS} meaningful characters for snippet analysis.`, + ...basePayload, + })); + return; + } + + if (triggerSource === 'auto' && normalizedSnippet.length > SNIPPET_MAX_AUTO_CHARS) { + setSnippetState((prev) => ({ + ...prev, + status: 'idle', + error: '', + notice: 'Large selection detected. Use Analyze Snippet for a manual run.', + ...basePayload, + })); + return; + } + + if (!analysisJobId || !selectedFilePath) { + setSnippetState((prev) => ({ + ...prev, + status: 'failed', + error: 'Analysis context is not ready yet. Load repository graph data and try again.', + notice: '', + ...basePayload, + })); + return; + } + + if (snippetDebounceRef.current) { + clearTimeout(snippetDebounceRef.current); + } + + snippetDebounceRef.current = setTimeout(async () => { + if (snippetAbortRef.current) { + snippetAbortRef.current.abort(); + } + + const controller = new AbortController(); + snippetAbortRef.current = controller; + + setSnippetState((prev) => ({ + ...prev, + status: 'loading', + error: '', + notice: '', + ...basePayload, + })); + + try { + const result = await aiService.analyzeSnippetImpact({ + jobId: analysisJobId, + filePath: selectedFilePath, + snippet: normalizedSnippet, + lineStart, + lineEnd, + signal: controller.signal, + }); + + if (controller.signal.aborted) return; + + setSnippetState({ + status: 'succeeded', + error: '', + notice: '', + ...basePayload, + data: result, + }); + if (!isSnippetPopoverPinned) { + setSnippetPopoverAnchor((prev) => ({ ...prev, visible: true })); + } + } catch (error) { + if (controller.signal.aborted || error?.name === 'CanceledError' || error?.name === 'AbortError') { + return; + } + + setSnippetState((prev) => ({ + ...prev, + status: 'failed', + error: + error?.response?.data?.error || + error?.message || + 'Failed to analyze selected snippet.', + notice: '', + })); + } + }, 450); + }; + + const handleTextareaSelection = (event) => { + const target = event?.target; + const value = String(target?.value || ''); + const start = Number.isInteger(target?.selectionStart) ? target.selectionStart : 0; + const end = Number.isInteger(target?.selectionEnd) ? target.selectionEnd : 0; + + if (!value || end <= start) { + triggerSnippetAnalysis({ snippet: '', lineStart: null, lineEnd: null, shouldAnalyze: false }); + if (!isSnippetPopoverPinned) { + setSnippetPopoverAnchor((prev) => ({ ...prev, visible: false })); + } + return; + } + + const selectedSnippet = value.slice(start, end).trim(); + const lineStart = getLineNumberFromOffset(value, start); + const lineEnd = getLineNumberFromOffset(value, end); + + const targetRect = target?.getBoundingClientRect?.(); + const clientX = event?.nativeEvent?.clientX; + const clientY = event?.nativeEvent?.clientY; + updateSnippetPopoverAnchor({ + x: Number.isFinite(clientX) ? clientX + 12 : (targetRect?.right || 300) - 24, + y: Number.isFinite(clientY) ? clientY - 12 : (targetRect?.top || 80) + 18, + visible: true, + }); + + triggerSnippetAnalysis({ + snippet: selectedSnippet, + lineStart, + lineEnd, + shouldAnalyze: isAutoSnippetAnalyze, + triggerSource: isAutoSnippetAnalyze ? 'auto' : 'manual-ready', + }); + }; + + const getRangeOffsetsFromCodeElement = (codeElement, selectionRange) => { + if (!codeElement || !selectionRange) return null; + + const preRange = selectionRange.cloneRange(); + preRange.selectNodeContents(codeElement); + preRange.setEnd(selectionRange.startContainer, selectionRange.startOffset); + + const selectionText = selectionRange.toString(); + const start = preRange.toString().length; + const end = start + selectionText.length; + + return { + start, + end, + text: selectionText, + }; + }; + + const handleViewerSelection = () => { + const selection = window.getSelection(); + const codeContainer = viewerCodeRef.current; + if (!selection || !codeContainer || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + + if (!codeContainer.contains(range.commonAncestorContainer)) { + return; + } + + const codeElement = codeContainer.querySelector('code'); + const rawContent = String(fileState.data?.content || ''); + const rangeOffsets = getRangeOffsetsFromCodeElement(codeElement, range); + + if (!rangeOffsets || rangeOffsets.end <= rangeOffsets.start) { + triggerSnippetAnalysis({ snippet: '', lineStart: null, lineEnd: null, shouldAnalyze: false }); + if (!isSnippetPopoverPinned) { + setSnippetPopoverAnchor((prev) => ({ ...prev, visible: false })); + } + return; + } + + const selectedSnippet = rawContent.slice(rangeOffsets.start, rangeOffsets.end).trim(); + const lineStart = getLineNumberFromOffset(rawContent, rangeOffsets.start); + const lineEnd = getLineNumberFromOffset(rawContent, rangeOffsets.end); + const rangeRect = range.getBoundingClientRect(); + updateSnippetPopoverAnchor({ + x: (rangeRect?.right || 300) + 10, + y: rangeRect?.top || 80, + visible: true, + }); + + triggerSnippetAnalysis({ + snippet: selectedSnippet, + lineStart, + lineEnd, + shouldAnalyze: isAutoSnippetAnalyze, + triggerSource: isAutoSnippetAnalyze ? 'auto' : 'manual-ready', + }); + }; + + const handleManualSnippetAnalyze = () => { + if (!snippetState.selectedSnippet) return; + + triggerSnippetAnalysis({ + snippet: snippetState.selectedSnippet, + lineStart: snippetState.lineStart, + lineEnd: snippetState.lineEnd, + shouldAnalyze: true, + triggerSource: 'manual', + }); + openSnippetDrawer(); + }; + + useEffect(() => { + const onKeyDown = (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + if (!snippetState.selectedSnippet) return; + event.preventDefault(); + handleManualSnippetAnalyze(); + return; + } + + if ((event.ctrlKey || event.metaKey) && event.shiftKey && String(event.key || '').toLowerCase() === 'i') { + event.preventDefault(); + openSnippetDrawer(); + return; + } + + if (event.key === 'Escape' && !isSnippetPopoverPinned) { + setSnippetPopoverAnchor((prev) => ({ ...prev, visible: false })); + } + }; + + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [isSnippetPopoverPinned, snippetState.selectedSnippet]); + + const showSnippetPopover = + snippetPopoverAnchor.visible && + Boolean(snippetState.selectedSnippet) && + !isMobileSnippetSheetOpen; + + const renderSnippetImpactDetails = ({ compact = false } = {}) => ( +
                  + {snippetState.data?.whatItDoes && ( +
                  +

                  + What It Does +

                  +

                  + {snippetState.data.whatItDoes} +

                  +
                  + )} + + {snippetState.data?.fileImpact && !compact && ( +
                  +

                  + File Impact +

                  +

                  + {snippetState.data.fileImpact} +

                  +
                  + )} + + {snippetState.data?.codebaseImpact && !compact && ( +
                  +

                  + Codebase Impact +

                  +

                  + {snippetState.data.codebaseImpact} +

                  +
                  + )} + + {(typeof snippetConfidenceScore === 'number' || snippetState.data?.confidence) && ( +
                  + Confidence: + + {snippetState.data?.confidence || 'unknown'} + + {typeof snippetConfidenceScore === 'number' && ( + ({snippetConfidenceScore.toFixed(2)}) + )} + {snippetState.data?.rerunTriggered && ( + - Re-analyzed for low confidence + )} +
                  + )} + + {!compact && + Array.isArray(snippetState.data?.directlyImpactedFiles) && + snippetState.data.directlyImpactedFiles.length > 0 && ( +
                  +

                  + Directly Impacted Files ({snippetState.data.directlyImpactedFiles.length}) +

                  +
                    + {snippetState.data.directlyImpactedFiles.slice(0, 8).map((file) => ( +
                  • + {file} +
                  • + ))} +
                  +
                  + )} + + {!compact && + Array.isArray(snippetState.data?.transitivelyImpactedFiles) && + snippetState.data.transitivelyImpactedFiles.length > 0 && ( +
                  +

                  + Transitively Impacted Files ({snippetState.data.transitivelyImpactedFiles.length}) +

                  +
                    + {snippetState.data.transitivelyImpactedFiles.slice(0, 8).map((file) => ( +
                  • + {file} +
                  • + ))} +
                  +
                  + )} +
                  + ); + + return ( +
                  +
                  + + + Back to Explorer + +
                  + +
                  + {structure.repository?.fullName && ( +
                  + + {structure.repository.fullName} + + + + {structure.repository.branch || structure.repository.defaultBranch || 'default'} + + {fileState.data?.htmlUrl && ( + + View on GitHub + + + )} +
                  + )} +
                  + + {!selectedFilePath && ( +
                  + No file selected. Open a file from repository explorer first. +
                  + )} + + {selectedFilePath && ( +
                  +
                  +
                  +
                  +

                  File

                  +

                  {selectedFilePath}

                  +

                  + Select code to analyze. Ctrl/Cmd+Enter runs snippet analysis. +

                  +
                  + +
                  +
                  +
                  + + +
                  + + + + {snippetStatusMeta.label} + + + {!isAutoSnippetAnalyze && ( + + )} +
                  + + {!isEditing ? ( + + ) : ( + <> + + + + )} +
                  +
                  + + {fileState.status === 'loading' && ( +
                  Loading file content...
                  + )} + + {fileState.status === 'failed' && fileState.error && ( +
                  + {fileState.error} +
                  + )} + + {fileState.status === 'succeeded' && fileState.data && ( +
                  + {isEditing ? ( +
                  +
                  + +