Se agrega componente de ia y pdf

This commit is contained in:
2025-11-13 10:02:26 -06:00
parent 0e884f20c5
commit 7105b286bf
6 changed files with 671 additions and 8 deletions

View File

@@ -32,6 +32,8 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"gsap": "^3.13.0",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0",
"next-themes": "^0.4.6",
"react": "^19.0.0",
@@ -482,12 +484,18 @@
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
"@types/phoenix": ["@types/phoenix@1.6.6", "", {}, "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="],
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
@@ -536,6 +544,8 @@
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -548,6 +558,8 @@
"canvas-confetti": ["canvas-confetti@1.9.3", "", {}, "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g=="],
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A=="],
@@ -578,6 +590,10 @@
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="],
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
"cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
@@ -628,6 +644,8 @@
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.204", "", {}, "sha512-s9VbBXWxfDrl67PlO4avwh0/GU2vcwx8Fph3wlR8LJl7ySGYId59EFE17VWVcuC3sLWNPENm6Z/uGqKbkPCcXA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
@@ -654,8 +672,12 @@
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -682,6 +704,8 @@
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -694,6 +718,8 @@
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
@@ -726,6 +752,10 @@
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jspdf": ["jspdf@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.26.9", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ=="],
"jspdf-autotable": ["jspdf-autotable@5.0.2", "", { "peerDependencies": { "jspdf": "^2 || ^3" } }, "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
@@ -836,6 +866,8 @@
"nwsapi": ["nwsapi@2.2.21", "", {}, "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA=="],
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
@@ -844,6 +876,8 @@
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@@ -858,6 +892,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
@@ -886,6 +922,8 @@
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
@@ -894,6 +932,8 @@
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
"rollup": ["rollup@4.46.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.3", "@rollup/rollup-android-arm64": "4.46.3", "@rollup/rollup-darwin-arm64": "4.46.3", "@rollup/rollup-darwin-x64": "4.46.3", "@rollup/rollup-freebsd-arm64": "4.46.3", "@rollup/rollup-freebsd-x64": "4.46.3", "@rollup/rollup-linux-arm-gnueabihf": "4.46.3", "@rollup/rollup-linux-arm-musleabihf": "4.46.3", "@rollup/rollup-linux-arm64-gnu": "4.46.3", "@rollup/rollup-linux-arm64-musl": "4.46.3", "@rollup/rollup-linux-loongarch64-gnu": "4.46.3", "@rollup/rollup-linux-ppc64-gnu": "4.46.3", "@rollup/rollup-linux-riscv64-gnu": "4.46.3", "@rollup/rollup-linux-riscv64-musl": "4.46.3", "@rollup/rollup-linux-s390x-gnu": "4.46.3", "@rollup/rollup-linux-x64-gnu": "4.46.3", "@rollup/rollup-linux-x64-musl": "4.46.3", "@rollup/rollup-win32-arm64-msvc": "4.46.3", "@rollup/rollup-win32-ia32-msvc": "4.46.3", "@rollup/rollup-win32-x64-msvc": "4.46.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw=="],
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
@@ -924,6 +964,8 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
@@ -934,6 +976,8 @@
"style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow=="],
"svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
@@ -944,6 +988,8 @@
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
@@ -1006,6 +1052,8 @@
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],

View File

@@ -0,0 +1,371 @@
import React, { useEffect, useState } from "react"
import { supabase } from "@/auth/supabase"
import type { PlanTextFields } from "../planes/academic-sections";
// 🔹 SIMULACIÓN DE ICONO LUCIDE-REACT
const Paperclip = (props) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-paperclip">
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
</svg>
);
// 🔹 SIMULACIÓN DE SHADCN/UI
const Dialog = ({ open, onOpenChange, children }) => open ? <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={onOpenChange}>{children}</div> : null;
const DialogContent = ({ className, children }) => <div className={`bg-white rounded-xl shadow-2xl transform transition-all max-w-6xl w-[95vw] h-[85vh] p-6 flex flex-col ${className}`} onClick={(e) => e.stopPropagation()}>{children}</div>;
const DialogHeader = ({ children }) => <div className="pb-4 border-b border-gray-200">{children}</div>;
const DialogTitle = ({ className, children }) => <h2 className={`text-xl font-bold ${className}`}>{children}</h2>;
const Button = ({ onClick, disabled, className, variant, children }) => (
<button
onClick={onClick}
disabled={disabled}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
variant === "outline" ? "bg-white border border-gray-300 text-gray-700 hover:bg-gray-50" :
"bg-blue-600 text-white hover:bg-blue-700"
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
>
{children}
</button>
);
const Card = ({ className, children }) => <div className={`bg-white rounded-2xl shadow-md ${className}`}>{children}</div>;
const CardContent = ({ className, children }) => <div className={`p-4 ${className}`}>{children}</div>;
const ScrollArea = ({ className, children }) => <div className={`overflow-y-auto ${className}`}>{children}</div>;
// ====================================================================
type AIChatModalProps = {
open: boolean
onClose: () => void
edgeFunctionUrl: string
context?: {
section?: string
fieldKey?: keyof PlanTextFields
originalText?: string
}
onAccept?: (newText: string) => void
}
export default function AIChatModal({ open, onClose, edgeFunctionUrl, context, onAccept }: AIChatModalProps) {
const [files, setFiles] = useState<any[]>([])
const [attachedFile, setAttachedFile] = useState<File | null>(null)
const [attachedPreview, setAttachedPreview] = useState<string | null>(null)
const [messages, setMessages] = useState<{ role: string; content: string }[]>([])
const [input, setInput] = useState("")
const [loading, setLoading] = useState(false)
// Referencia para desplazar al final del chat
const messagesEndRef = React.useRef<HTMLDivElement>(null);
const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
useEffect(scrollToBottom, [messages])
useEffect(() => {
if (!open) {
// 🧹 Limpia mensajes y archivos al cerrar
setMessages([])
setInput("")
setAttachedFile(null)
setAttachedPreview(null)
} else if (context) {
// 🧩 Muestra el contexto inicial al abrir
setMessages([
{
role: "system",
content: `Contexto: ${context.section}\nTexto original:\n${context.originalText || "—"}`,
},
])
}
}, [open, context])
// 🔹 Obtener lista de archivos del Vector Store (Lógica de API original)
useEffect(() => {
if (!open) return
const fetchVectorFiles = async () => {
// Nota: La verificación de Supabase ahora pasa por el mock.
if (typeof supabase === 'undefined' || !supabase.auth) {
console.error("Supabase no está disponible (Simulación). Saltando fetch de archivos.");
return;
}
try {
setLoading(true)
const { data: { session } } = await supabase.auth.getSession()
const token = session?.access_token
// 🟢 TU LÓGICA DE FETCH ORIGINAL
const res = await fetch(`${edgeFunctionUrl}?action=list_files`, {
headers: { Authorization: `Bearer ${token}` },
})
const data = await res.json()
if (data.files) setFiles(data.files)
else console.warn("No se encontraron archivos en el vector store.")
} catch (err) {
console.error("Error al cargar archivos del vector store:", err)
} finally {
setLoading(false)
}
}
fetchVectorFiles()
}, [open, edgeFunctionUrl])
// 📎 Adjuntar archivo
const handleAttach = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setAttachedFile(file)
setAttachedPreview(file.name)
}
// 🚀 Enviar prompt o archivo al Edge Function (Lógica de API original)
const handleSend = async () => {
if (!input.trim() && !attachedFile) return
// 🧩 Crear el mensaje visible del usuario
let userMessageContent = input.trim()
if (attachedFile) {
userMessageContent += (userMessageContent ? " | " : "") + `Adjunto: ${attachedFile.name}`
}
if (!userMessageContent && attachedFile) {
userMessageContent = `Consulta de archivo: ${attachedFile.name}`
}
setMessages((prev) => [...prev, { role: "user", content: userMessageContent }])
setInput("")
setLoading(true)
try {
if (typeof supabase === "undefined" || !supabase.functions) {
throw new Error("Supabase no está disponible o no soporta Edge Functions.")
}
const formData = new FormData()
// 🧠 Construimos un prompt limpio con el contexto del campo
const contextText = context?.originalText || "Sin texto original"
const section = context?.section ? `Sección: ${context.section}` : ""
const field = context?.fieldKey ? `Campo: ${context.fieldKey}` : ""
const fullPrompt = `
${section}
${field}
Texto original:
${contextText}
Solicitud del usuario:
${input}
Responde con una versión mejorada del texto, sin agregar frases como “Aquí tienes” ni explicaciones.
`.trim()
formData.append("prompt", fullPrompt)
if (attachedFile) formData.append("file", attachedFile)
// 🟢 Llamada a la Edge Function
const { data, error } = await supabase.functions.invoke("simple-chat", {
body: formData,
})
if (error) throw error
setMessages((prev) => [
...prev,
{ role: "assistant", content: data?.text || "Sin respuesta del modelo." },
])
} catch (err: any) {
console.error("Error al enviar prompt:", err)
setMessages((prev) => [
...prev,
{ role: "assistant", content: "Ocurrió un error al conectar con la API." },
])
} finally {
setLoading(false)
setAttachedFile(null)
setAttachedPreview(null)
}
}
return (
    <Dialog open={open} onOpenChange={onClose}>
      {/* DialogContent ya define el tamaño h-[85vh] y es flex-col */}
      <DialogContent className="max-w-6xl w-[95vw] h-[85vh] p-6 flex flex-col">
       
{/* Encabezado fijo (flex-shrink-0) */}
        <DialogHeader>
          <DialogTitle className="text-lg font-semibold">Asistente Inteligente</DialogTitle>
        </DialogHeader>
{/* CONTENEDOR PRINCIPAL QUE AHORA GESTIONA EL SCROLL DEL CONTENIDO */}
{/* flex-1: toma el espacio restante. overflow-y-auto: permite scroll si el contenido se desborda */}
        <div className="flex-1 overflow-y-auto pt-4">
{/* Contenido que originalmente estaba justo debajo del header */}
<div className="flex gap-6 min-h-full">
          {/* 📂 Archivos del Vector Store */}
          <Card className="w-1/3 min-w-[250px] max-w-sm flex flex-col bg-muted/20 border border-gray-200 rounded-2xl">
            {/* Se mantiene flex-1 en CardContent para el ScrollArea */}
            <CardContent className="flex flex-col flex-1 p-4">
              <h3 className="font-semibold text-sm mb-3">Archivos del Vector Store</h3>
              {/* ScrollArea toma el espacio disponible (flex-1) */}
              <ScrollArea className="flex-1">
                {files.length === 0 ? (
                  <p className="text-gray-500 text-sm text-center mt-10">
                    {loading ? "Cargando archivos..." : "No hay archivos cargados."}
                  </p>
                ) : (
                  <ul className="space-y-2">
                    {files.map((file) => (
                      <li
                        key={file.id}
                        className="border border-gray-200 bg-white rounded-lg p-2 text-sm shadow-sm hover:shadow-lg transition-shadow cursor-pointer"
                      >
                        <strong className="block text-gray-700 truncate">{file.name}</strong>
                        <p className="text-xs text-gray-400 truncate">{file.path}</p>
                      </li>
                    ))}
                  </ul>
                )}
              </ScrollArea>
              <div className="mt-4 flex-shrink-0">
                <Button variant="outline" className="w-full">
                  Subir archivo
                </Button>
              </div>
            </CardContent>
          </Card>
          {/* 💬 Chat con GPT */}
          <Card className="flex-1 flex flex-col min-w-[350px] bg-background border border-gray-200 rounded-2xl">
            <CardContent className="flex flex-col flex-1 p-4">
              <h3 className="font-semibold text-sm mb-3 flex-shrink-0">Chat con IA</h3>
              {/* Mensajes - EL CONTENEDOR QUE DEBE HACER SCROLL */}
               <div className="flex-1 overflow-y-auto min-h-0 border border-gray-200 rounded-lg p-3 space-y-3 bg-gray-50 break-words whitespace-pre-wrap">
                {messages.length === 0 ? (
                  <p className="text-gray-400 text-sm text-center mt-10">
                    Inicia una conversación...
                  </p>
                ) : (
                  messages.map((m, i) => (
                    <div
                      key={i}
                      className={`break-words whitespace-pre-wrap p-3 rounded-xl shadow-sm max-w-[85%] ${
                        m.role === "user"
                          ? "bg-blue-50 text-blue-800 ml-auto"
                          : "bg-white text-gray-800 mr-auto border border-gray-200"
                      }`}
                    >
                      <strong className="font-bold">{m.role === "user" ? "Tú:" : "IA:"}</strong>{" "}
                      {m.content}
                    </div>
                  ))
                )}
                {loading && (
                  <div className="flex items-center space-x-2 p-3 bg-white border border-gray-200 rounded-xl mr-auto max-w-fit shadow-sm flex-shrink-0">
                    <svg
                      className="animate-spin h-4 w-4 text-blue-500"
                      xmlns="http://www.w3.org/2000/svg"
                      fill="none"
                      viewBox="0 0 24 24"
                    >
                      <circle
                        className="opacity-25"
                        cx="12"
                        cy="12"
                        r="10"
                        stroke="currentColor"
                        strokeWidth="4"
                      ></circle>
                      <path
                        className="opacity-75"
                        fill="currentColor"
                        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2
                        5.291A7.962 7.962 0 014 12H0c0
                        3.042 1.135 5.824 3 7.938l3-2.647z"
                      ></path>
                    </svg>
                    <span className="text-sm text-gray-600">La IA está respondiendo...</span>
                  </div>
                )}
                <div ref={messagesEndRef} />
              </div>
              {/* Archivo adjunto - flex-shrink-0 */}
              {attachedPreview && (
                <div className="flex items-center justify-between mt-2 p-3 border border-gray-300 rounded-xl text-sm bg-gray-100 shadow-inner flex-shrink-0">
                  <span className="truncate flex items-center gap-2 text-gray-700">
                    <Paperclip className="w-4 h-4 text-blue-500" />
                    {attachedPreview}
                  </span>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="text-red-500 hover:bg-gray-200"
                    onClick={() => {
                      setAttachedFile(null)
                      setAttachedPreview(null)
                      setInput("")
                    }}
                  >
                    Quitar
                  </Button>
                </div>
              )}
              {/* Entrada de texto y enviar - flex-shrink-0 */}
              <div className="flex gap-2 mt-4 items-end flex-shrink-0">
                <label className="cursor-pointer text-gray-600 hover:text-blue-600 self-center">
                  <Paperclip className="w-5 h-5" />
                  <input type="file" className="hidden" onChange={handleAttach} />
                </label>
                <textarea
                  value={input}
                  onChange={(e) => setInput(e.target.value)}
                  placeholder="Escribe tu pregunta..."
                  onKeyDown={(e) => {
                    if (e.key === "Enter" && !e.shiftKey) {
                      e.preventDefault()
                      handleSend()
                    }
                  }}
                  rows={1}
                  className="flex-1 resize-none rounded-xl border border-gray-300 p-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 max-h-32 overflow-y-auto bg-white shadow-inner"
                  style={{ minHeight: "38px" }}
                />
                <Button
                  onClick={handleSend}
                  disabled={loading || (!input.trim() && !attachedFile)}
                  className="shadow-md"
                >
                  {loading ? "Enviando..." : "Enviar"}
                </Button>
<Button
onClick={() => {
const lastMessage = messages[messages.length - 1]
if (onAccept && lastMessage?.role === "assistant") {
onAccept(lastMessage.content)
onClose()
}
}}
disabled={!messages.some((m) => m.role === "assistant")}
className="shadow-md"
>
Aplicar mejora
</Button>
              </div>
            </CardContent>
          </Card>
        </div>
</div>
      </DialogContent>
    </Dialog>
  )
}

View File

@@ -8,6 +8,7 @@ import { supabase,useSupabaseAuth } from "@/auth/supabase"
import { toast } from "sonner"
import ReactMarkdown from 'react-markdown'
import { HistorialCambiosModal } from "../historico/HistorialCambiosModal"
import AIChatModal from "../ai/AIChatModal"
/* =====================================================
@@ -115,6 +116,7 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
const qc = useQueryClient()
const auth = useSupabaseAuth()
const [openHistorial, setOpenHistorial] = useState(false)
const [openModalIa, setopenModalIa] = useState(false)
if(!planId) return <div>Cargando</div>
const { data: plan } = useSuspenseQuery(planTextOptions(planId))
@@ -162,6 +164,8 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
],
[]
)
const [iaContext, setIaContext] = useState<{ key: keyof PlanTextFields; title: string; content: string } | null>(null)
return (
<>
@@ -171,9 +175,14 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
return (
<SectionPanel key={s.id} id={s.id} title={s.title} icon={s.icon} color={color}>
{s.key === "historico" ? (
<Button variant="outline" size="sm" onClick={() => setOpenHistorial(true)}>
Ver historial
</Button>
<>
<Button variant="outline" size="sm" onClick={() => setOpenHistorial(true)}>
Ver historial
</Button>
<Button variant="outline" size="sm" onClick={() => setopenModalIa(true)}>
Promt
</Button>
</>
) : (
<>
<ExpandableText text={text} mono={s.mono} />
@@ -190,7 +199,7 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
Copiar
</Button>
{s.key !== "prompt" && (
<Button
<Button
variant="ghost"
size="sm"
onClick={() => {
@@ -268,6 +277,24 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
>
{updateField.isPending ? "Guardando…" : "Guardar"}
</Button>
<Button
variant="secondary"
onClick={() => {
if (!editing) return
const current = draft
setIaContext({
key: editing.key,
title: editing.title,
content: current,
})
setopenModalIa(true)
setEditing(null) // 🔹 Cierra el modal de edición
}}
>
Mejorar con IA
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -279,6 +306,24 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
updateField.mutate({ key, value })
}}
/>
<AIChatModal
open={openModalIa}
onClose={() => setopenModalIa(false)}
edgeFunctionUrl="https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/simple-chat"
context={{
section: iaContext?.title,
fieldKey: iaContext?.key,
originalText: iaContext?.content,
}}
onAccept={(newText: string) => {
if (iaContext) {
updateField.mutate({ key: iaContext.key, value: newText })
setIaContext(null)
}
}}
/>
</>
)
}

View File

@@ -0,0 +1,200 @@
"use client"
import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from "react"
type PromptInputContextType = {
isLoading: boolean
value: string
setValue: (value: string) => void
maxHeight: number | string
onSubmit?: () => void
disabled?: boolean
textareaRef: React.RefObject<HTMLTextAreaElement | null>
}
const PromptInputContext = createContext<PromptInputContextType>({
isLoading: false,
value: "",
setValue: () => {},
maxHeight: 240,
onSubmit: undefined,
disabled: false,
textareaRef: React.createRef<HTMLTextAreaElement>(),
})
function usePromptInput() {
const context = useContext(PromptInputContext)
if (!context) {
throw new Error("usePromptInput must be used within a PromptInput")
}
return context
}
type PromptInputProps = {
isLoading?: boolean
value?: string
onValueChange?: (value: string) => void
maxHeight?: number | string
onSubmit?: () => void
children: React.ReactNode
className?: string
}
function PromptInput({
className,
isLoading = false,
maxHeight = 240,
value,
onValueChange,
onSubmit,
children,
}: PromptInputProps) {
const [internalValue, setInternalValue] = useState(value || "")
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleChange = (newValue: string) => {
setInternalValue(newValue)
onValueChange?.(newValue)
}
return (
<TooltipProvider>
<PromptInputContext.Provider
value={{
isLoading,
value: value ?? internalValue,
setValue: onValueChange ?? handleChange,
maxHeight,
onSubmit,
textareaRef,
}}
>
<div
className={cn(
"border-input bg-background cursor-text rounded-3xl border p-2 shadow-xs",
className
)}
onClick={() => textareaRef.current?.focus()}
>
{children}
</div>
</PromptInputContext.Provider>
</TooltipProvider>
)
}
export type PromptInputTextareaProps = {
disableAutosize?: boolean
} & React.ComponentProps<typeof Textarea>
function PromptInputTextarea({
className,
onKeyDown,
disableAutosize = false,
...props
}: PromptInputTextareaProps) {
const { value, setValue, maxHeight, onSubmit, disabled, textareaRef } =
usePromptInput()
useEffect(() => {
if (disableAutosize) return
if (!textareaRef.current) return
if (textareaRef.current.scrollTop === 0) {
textareaRef.current.style.height = "auto"
}
textareaRef.current.style.height =
typeof maxHeight === "number"
? `${Math.min(textareaRef.current.scrollHeight, maxHeight)}px`
: `min(${textareaRef.current.scrollHeight}px, ${maxHeight})`
}, [value, maxHeight, disableAutosize])
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
onSubmit?.()
}
onKeyDown?.(e)
}
return (
<Textarea
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
className={cn(
"text-primary min-h-[44px] w-full resize-none border-none bg-transparent shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
className
)}
rows={1}
disabled={disabled}
{...props}
/>
)
}
type PromptInputActionsProps = React.HTMLAttributes<HTMLDivElement>
function PromptInputActions({
children,
className,
...props
}: PromptInputActionsProps) {
return (
<div className={cn("flex items-center gap-2", className)} {...props}>
{children}
</div>
)
}
type PromptInputActionProps = {
className?: string
tooltip: React.ReactNode
children: React.ReactNode
side?: "top" | "bottom" | "left" | "right"
} & React.ComponentProps<typeof Tooltip>
function PromptInputAction({
tooltip,
children,
className,
side = "top",
...props
}: PromptInputActionProps) {
const { disabled } = usePromptInput()
return (
<Tooltip {...props}>
<TooltipTrigger asChild disabled={disabled} onClick={event => event.stopPropagation()}>
{children}
</TooltipTrigger>
<TooltipContent side={side} className={className}>
{tooltip}
</TooltipContent>
</Tooltip>
)
}
export {
PromptInput,
PromptInputTextarea,
PromptInputActions,
PromptInputAction,
}

View File

@@ -44,13 +44,13 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)

View File

@@ -138,7 +138,6 @@
}
}
.animate-aurora {
background: radial-gradient(at 20% 30%, oklch(27.5% 0.13488 262.73), transparent 50%),
radial-gradient(at 80% 70%, oklch(0.704 0.191 22.216), transparent 50%),