Primera version de chunkeo completo crud
This commit is contained in:
323
frontend/package-lock.json
generated
323
frontend/package-lock.json
generated
@@ -11,7 +11,10 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.543.0",
|
||||
@@ -878,6 +881,44 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1204,12 +1245,41 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
|
||||
@@ -1240,6 +1310,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -1305,6 +1401,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
@@ -1413,6 +1524,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-rect": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1",
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
@@ -1484,6 +1627,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
@@ -1502,6 +1719,65 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
|
||||
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@@ -1602,6 +1878,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
@@ -1620,6 +1914,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.34",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.34.tgz",
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.543.0",
|
||||
|
||||
228
frontend/src/components/ChunkPreviewPanel.tsx
Normal file
228
frontend/src/components/ChunkPreviewPanel.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { api } from '../services/api'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import { AlertCircle, Loader2, FileText, CheckCircle2, XCircle } from 'lucide-react'
|
||||
import type { ChunkingConfig } from './ChunkingConfigModal'
|
||||
|
||||
interface ChunkPreviewPanelProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
config: ChunkingConfig | null
|
||||
onAccept: (config: ChunkingConfig) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
interface PreviewChunk {
|
||||
index: number
|
||||
text: string
|
||||
page: number
|
||||
file_name: string
|
||||
tokens: number
|
||||
}
|
||||
|
||||
export function ChunkPreviewPanel({
|
||||
isOpen,
|
||||
onClose,
|
||||
config,
|
||||
onAccept,
|
||||
onCancel,
|
||||
}: ChunkPreviewPanelProps) {
|
||||
const [chunks, setChunks] = useState<PreviewChunk[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
// Auto-cargar preview cuando se abre el modal
|
||||
useEffect(() => {
|
||||
if (isOpen && config && chunks.length === 0) {
|
||||
loadPreview()
|
||||
}
|
||||
}, [isOpen, config])
|
||||
|
||||
const loadPreview = async () => {
|
||||
if (!config) return
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setSuccess(false)
|
||||
|
||||
try {
|
||||
const result = await api.generateChunkPreview(config)
|
||||
setChunks(result.chunks)
|
||||
} catch (err) {
|
||||
console.error('Error loading preview:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error generando preview')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAccept = async () => {
|
||||
if (!config) return
|
||||
|
||||
setProcessing(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await onAccept(config)
|
||||
setSuccess(true)
|
||||
|
||||
// Cerrar después de 2 segundos
|
||||
setTimeout(() => {
|
||||
handleClose()
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
console.error('Error processing:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error procesando PDF')
|
||||
} finally {
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setChunks([])
|
||||
setError(null)
|
||||
setSuccess(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!config) return null
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
Preview de Chunks
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Vista previa de chunks para <strong>{config.file_name}</strong>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Contenido */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-gray-500">Generando preview...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 p-4 rounded">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : success ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<CheckCircle2 className="w-16 h-16 text-green-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-green-700">
|
||||
Procesamiento Completado
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
El PDF ha sido procesado y subido a Qdrant exitosamente
|
||||
</p>
|
||||
</div>
|
||||
) : chunks.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>No hay chunks para mostrar</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Información de configuración */}
|
||||
<div className="bg-blue-50 p-3 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Configuración:</strong> Max {config.max_tokens} tokens, Target{' '}
|
||||
{config.target_tokens} tokens
|
||||
{config.use_llm && ' | LLM Habilitado'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Lista de chunks */}
|
||||
{chunks.map((chunk) => (
|
||||
<div key={chunk.index} className="border rounded-lg p-4 space-y-2">
|
||||
{/* Header del chunk */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-700">
|
||||
Chunk #{chunk.index + 1}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded">
|
||||
Página {chunk.page}
|
||||
</span>
|
||||
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-1 rounded">
|
||||
~{chunk.tokens} tokens
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Texto del chunk */}
|
||||
<div className="bg-gray-50 p-3 rounded text-sm">
|
||||
<p className="text-gray-700 whitespace-pre-wrap leading-relaxed">
|
||||
{chunk.text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Indicador de longitud */}
|
||||
<div className="text-xs text-gray-500">
|
||||
Longitud: {chunk.text.length} caracteres
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Información adicional */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 p-3 rounded">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Nota:</strong> Estos son chunks de ejemplo (hasta 3). El documento
|
||||
completo generará más chunks según su tamaño.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer con acciones */}
|
||||
<DialogFooter className="flex justify-between items-center pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={processing || success}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
Cancelar
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleAccept} disabled={processing || loading || chunks.length === 0 || success}>
|
||||
{processing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Procesando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
Aceptar y Procesar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
205
frontend/src/components/ChunkViewerModal.tsx
Normal file
205
frontend/src/components/ChunkViewerModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../services/api'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import { AlertCircle, Loader2, FileText, Trash2 } from 'lucide-react'
|
||||
|
||||
interface ChunkViewerModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
fileName: string
|
||||
tema: string
|
||||
}
|
||||
|
||||
interface Chunk {
|
||||
id: string
|
||||
payload: {
|
||||
page_content: string
|
||||
metadata: {
|
||||
file_name: string
|
||||
page: number
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
vector?: number[]
|
||||
}
|
||||
|
||||
export function ChunkViewerModal({ isOpen, onClose, fileName, tema }: ChunkViewerModalProps) {
|
||||
const [chunks, setChunks] = useState<Chunk[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && fileName && tema) {
|
||||
loadChunks()
|
||||
}
|
||||
}, [isOpen, fileName, tema])
|
||||
|
||||
const loadChunks = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await api.getChunksByFile(tema, fileName)
|
||||
setChunks(result.chunks)
|
||||
} catch (err) {
|
||||
console.error('Error loading chunks:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error al cargar chunks')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteFile = async () => {
|
||||
if (!confirm(`¿Estás seguro de eliminar todos los chunks del archivo "${fileName}" de la colección "${tema}"?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
setDeleting(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await api.deleteFileFromCollection(tema, fileName)
|
||||
alert('Archivo eliminado de la colección exitosamente')
|
||||
onClose()
|
||||
} catch (err) {
|
||||
console.error('Error deleting file from collection:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error al eliminar archivo')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setChunks([])
|
||||
setError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
Chunks de "{fileName}"
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Colección: <strong>{tema}</strong>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Contenido */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-gray-500">Cargando chunks...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 p-4 rounded">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : chunks.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>No se encontraron chunks para este archivo.</p>
|
||||
<p className="text-sm mt-1">El archivo aún no ha sido procesado o no existe en la colección.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Estadísticas */}
|
||||
<div className="bg-blue-50 p-3 rounded">
|
||||
<p className="text-sm text-blue-800">
|
||||
<strong>Total de chunks:</strong> {chunks.length}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Lista de chunks */}
|
||||
{chunks.map((chunk, index) => (
|
||||
<div key={chunk.id} className="border rounded-lg p-4 space-y-2">
|
||||
{/* Header del chunk */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-700">
|
||||
Chunk #{index + 1}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
Página {chunk.payload.metadata.page}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 font-mono">
|
||||
ID: {chunk.id.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Texto del chunk */}
|
||||
{chunk.payload.page_content && (
|
||||
<div className="bg-gray-50 p-3 rounded text-sm">
|
||||
<p className="text-gray-700 whitespace-pre-wrap">
|
||||
{chunk.payload.page_content}
|
||||
</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
<strong>Caracteres:</strong> {chunk.payload.page_content.length}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="text-xs text-gray-500">
|
||||
<strong>Metadata:</strong>
|
||||
<pre className="mt-1 bg-gray-100 p-2 rounded overflow-x-auto">
|
||||
{JSON.stringify(chunk.payload.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Información del vector (opcional) */}
|
||||
{chunk.vector && (
|
||||
<div className="text-xs text-gray-400">
|
||||
Vector dimension: {chunk.vector.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer con acciones */}
|
||||
<div className="flex justify-between items-center pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDeleteFile}
|
||||
disabled={deleting || chunks.length === 0}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
{deleting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Eliminando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Eliminar de colección
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleClose}>
|
||||
Cerrar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
307
frontend/src/components/ChunkingConfigModal.tsx
Normal file
307
frontend/src/components/ChunkingConfigModal.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../services/api'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import { Label } from './ui/label'
|
||||
import { Input } from './ui/input'
|
||||
import { Textarea } from './ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'
|
||||
import { Switch } from './ui/switch'
|
||||
import { AlertCircle, Loader2, Settings, Sparkles } from 'lucide-react'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'
|
||||
|
||||
interface ChunkingConfigModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
fileName: string
|
||||
tema: string
|
||||
collectionName: string
|
||||
onPreview: (config: ChunkingConfig) => void
|
||||
}
|
||||
|
||||
export interface ChunkingConfig {
|
||||
file_name: string
|
||||
tema: string
|
||||
collection_name: string
|
||||
max_tokens: number
|
||||
target_tokens: number
|
||||
chunk_size: number
|
||||
chunk_overlap: number
|
||||
use_llm: boolean
|
||||
custom_instructions: string
|
||||
}
|
||||
|
||||
interface ChunkingProfile {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
max_tokens: number
|
||||
target_tokens: number
|
||||
chunk_size: number
|
||||
chunk_overlap: number
|
||||
use_llm: boolean
|
||||
}
|
||||
|
||||
export function ChunkingConfigModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
fileName,
|
||||
tema,
|
||||
collectionName,
|
||||
onPreview,
|
||||
}: ChunkingConfigModalProps) {
|
||||
const [profiles, setProfiles] = useState<ChunkingProfile[]>([])
|
||||
const [selectedProfile, setSelectedProfile] = useState<string>('balanced')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Custom configuration
|
||||
const [maxTokens, setMaxTokens] = useState(950)
|
||||
const [targetTokens, setTargetTokens] = useState(800)
|
||||
const [chunkSize, setChunkSize] = useState(1000)
|
||||
const [chunkOverlap, setChunkOverlap] = useState(200)
|
||||
const [useLLM, setUseLLM] = useState(true)
|
||||
const [customInstructions, setCustomInstructions] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadProfiles()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const loadProfiles = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await api.getChunkingProfiles()
|
||||
setProfiles(result.profiles)
|
||||
} catch (err) {
|
||||
console.error('Error loading profiles:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error cargando perfiles')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleProfileChange = (profileId: string) => {
|
||||
setSelectedProfile(profileId)
|
||||
const profile = profiles.find((p) => p.id === profileId)
|
||||
if (profile) {
|
||||
setMaxTokens(profile.max_tokens)
|
||||
setTargetTokens(profile.target_tokens)
|
||||
setChunkSize(profile.chunk_size)
|
||||
setChunkOverlap(profile.chunk_overlap)
|
||||
setUseLLM(profile.use_llm)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = () => {
|
||||
const config: ChunkingConfig = {
|
||||
file_name: fileName,
|
||||
tema: tema,
|
||||
collection_name: collectionName,
|
||||
max_tokens: maxTokens,
|
||||
target_tokens: targetTokens,
|
||||
chunk_size: chunkSize,
|
||||
chunk_overlap: chunkOverlap,
|
||||
use_llm: useLLM,
|
||||
custom_instructions: useLLM ? customInstructions : '',
|
||||
}
|
||||
onPreview(config)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setError(null)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
Configurar Chunking
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configura cómo se procesará el archivo <strong>{fileName}</strong>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-gray-500">Cargando perfiles...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 p-4 rounded">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs defaultValue="profiles" className="flex-1">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="profiles">Perfiles</TabsTrigger>
|
||||
<TabsTrigger value="custom">Personalizado</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab de Perfiles */}
|
||||
<TabsContent value="profiles" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Perfil de Configuración</Label>
|
||||
<Select value={selectedProfile} onValueChange={handleProfileChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecciona un perfil" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{profile.name}</span>
|
||||
<span className="text-xs text-gray-500">{profile.description}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Mostrar detalles del perfil seleccionado */}
|
||||
{selectedProfile && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-2 text-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<span className="font-medium">Max Tokens:</span> {maxTokens}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Target Tokens:</span> {targetTokens}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Chunk Size:</span> {chunkSize}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Overlap:</span> {chunkOverlap}
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="font-medium">LLM:</span>{' '}
|
||||
{useLLM ? '✅ Habilitado' : '❌ Deshabilitado'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab Personalizado */}
|
||||
<TabsContent value="custom" className="space-y-4 overflow-y-auto max-h-[50vh]">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="maxTokens">Max Tokens</Label>
|
||||
<Input
|
||||
id="maxTokens"
|
||||
type="number"
|
||||
min={100}
|
||||
max={2000}
|
||||
value={maxTokens}
|
||||
onChange={(e) => setMaxTokens(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="targetTokens">Target Tokens</Label>
|
||||
<Input
|
||||
id="targetTokens"
|
||||
type="number"
|
||||
min={100}
|
||||
max={2000}
|
||||
value={targetTokens}
|
||||
onChange={(e) => setTargetTokens(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="chunkSize">Chunk Size</Label>
|
||||
<Input
|
||||
id="chunkSize"
|
||||
type="number"
|
||||
min={100}
|
||||
max={3000}
|
||||
value={chunkSize}
|
||||
onChange={(e) => setChunkSize(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="chunkOverlap">Chunk Overlap</Label>
|
||||
<Input
|
||||
id="chunkOverlap"
|
||||
type="number"
|
||||
min={0}
|
||||
max={1000}
|
||||
value={chunkOverlap}
|
||||
onChange={(e) => setChunkOverlap(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle LLM */}
|
||||
<div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-blue-600" />
|
||||
<div>
|
||||
<Label htmlFor="useLLM" className="font-medium cursor-pointer">
|
||||
Usar LLM (Gemini)
|
||||
</Label>
|
||||
<p className="text-xs text-gray-600">
|
||||
Procesamiento inteligente con IA
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="useLLM"
|
||||
checked={useLLM}
|
||||
onCheckedChange={setUseLLM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom Instructions (solo si LLM está habilitado) */}
|
||||
{useLLM && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="customInstructions">
|
||||
Instrucciones Personalizadas (Opcional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="customInstructions"
|
||||
placeholder="Ej: Mantén todos los términos técnicos en inglés..."
|
||||
value={customInstructions}
|
||||
onChange={(e) => setCustomInstructions(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Instrucciones adicionales para guiar el procesamiento con IA
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
<DialogFooter className="flex justify-between items-center pt-4 border-t">
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handlePreview} disabled={loading}>
|
||||
Generar Preview
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
164
frontend/src/components/CollectionVerifier.tsx
Normal file
164
frontend/src/components/CollectionVerifier.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../services/api'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import { AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'
|
||||
|
||||
interface CollectionVerifierProps {
|
||||
tema: string | null
|
||||
onVerified?: (exists: boolean) => void
|
||||
}
|
||||
|
||||
export function CollectionVerifier({ tema, onVerified }: CollectionVerifierProps) {
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
const [collectionExists, setCollectionExists] = useState<boolean | null>(null)
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (tema) {
|
||||
checkCollection()
|
||||
} else {
|
||||
setCollectionExists(null)
|
||||
}
|
||||
}, [tema])
|
||||
|
||||
const checkCollection = async () => {
|
||||
if (!tema) return
|
||||
|
||||
setIsChecking(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await api.checkCollectionExists(tema)
|
||||
setCollectionExists(result.exists)
|
||||
|
||||
// Si no existe, mostrar el diálogo de confirmación
|
||||
if (!result.exists) {
|
||||
setShowCreateDialog(true)
|
||||
}
|
||||
|
||||
onVerified?.(result.exists)
|
||||
} catch (err) {
|
||||
console.error('Error checking collection:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error al verificar colección')
|
||||
setCollectionExists(null)
|
||||
} finally {
|
||||
setIsChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateCollection = async () => {
|
||||
if (!tema) return
|
||||
|
||||
setIsCreating(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await api.createCollection(tema)
|
||||
|
||||
if (result.success) {
|
||||
setCollectionExists(true)
|
||||
setShowCreateDialog(false)
|
||||
onVerified?.(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating collection:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error al crear colección')
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setShowCreateDialog(false)
|
||||
// Opcionalmente podemos notificar que no se creó la colección
|
||||
onVerified?.(false)
|
||||
}
|
||||
|
||||
// No renderizar nada si no hay tema seleccionado
|
||||
if (!tema) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Indicador de estado de la colección */}
|
||||
{isChecking ? (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-4">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Verificando colección...</span>
|
||||
</div>
|
||||
) : collectionExists === true ? (
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 mb-4">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>Colección "{tema}" disponible en Qdrant</span>
|
||||
</div>
|
||||
) : collectionExists === false ? (
|
||||
<div className="flex items-center gap-2 text-sm text-yellow-600 mb-4">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>Colección "{tema}" no existe en Qdrant</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 mb-4">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Diálogo de confirmación para crear colección */}
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Crear colección en Qdrant</DialogTitle>
|
||||
<DialogDescription>
|
||||
La colección "<strong>{tema}</strong>" no existe en la base de datos vectorial.
|
||||
<br />
|
||||
<br />
|
||||
¿Deseas crear esta colección ahora? Esto permitirá almacenar y buscar chunks de
|
||||
documentos para este tema.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 p-3 rounded">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancelCreate}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateCollection}
|
||||
disabled={isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Creando...
|
||||
</>
|
||||
) : (
|
||||
'Crear colección'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,10 @@ import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { FileUpload } from './FileUpload'
|
||||
import { DeleteConfirmDialog } from './DeleteConfirmDialog'
|
||||
import { PDFPreviewModal } from './PDFPreviewModal'
|
||||
import { CollectionVerifier } from './CollectionVerifier'
|
||||
import { ChunkViewerModal } from './ChunkViewerModal'
|
||||
import { ChunkingConfigModal, type ChunkingConfig } from './ChunkingConfigModal'
|
||||
import { ChunkPreviewPanel } from './ChunkPreviewPanel'
|
||||
import {
|
||||
Upload,
|
||||
Download,
|
||||
@@ -22,7 +26,8 @@ import {
|
||||
Search,
|
||||
FileText,
|
||||
Eye,
|
||||
MessageSquare
|
||||
MessageSquare,
|
||||
Scissors
|
||||
} from 'lucide-react'
|
||||
|
||||
export function Dashboard() {
|
||||
@@ -52,6 +57,20 @@ export function Dashboard() {
|
||||
const [previewFileTema, setPreviewFileTema] = useState<string | undefined>(undefined)
|
||||
const [loadingPreview, setLoadingPreview] = useState(false)
|
||||
|
||||
// Estados para el modal de chunks
|
||||
const [chunkViewerOpen, setChunkViewerOpen] = useState(false)
|
||||
const [chunkFileName, setChunkFileName] = useState('')
|
||||
const [chunkFileTema, setChunkFileTema] = useState('')
|
||||
|
||||
// Estados para chunking
|
||||
const [chunkingConfigOpen, setChunkingConfigOpen] = useState(false)
|
||||
const [chunkingFileName, setChunkingFileName] = useState('')
|
||||
const [chunkingFileTema, setChunkingFileTema] = useState('')
|
||||
const [chunkingCollectionName, setChunkingCollectionName] = useState('')
|
||||
|
||||
const [chunkPreviewOpen, setChunkPreviewOpen] = useState(false)
|
||||
const [chunkingConfig, setChunkingConfig] = useState<ChunkingConfig | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles()
|
||||
}, [selectedTema])
|
||||
@@ -173,6 +192,54 @@ export function Dashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
// Abrir modal de chunks
|
||||
const handleViewChunks = (filename: string, tema: string) => {
|
||||
if (!tema) {
|
||||
alert('No hay tema seleccionado. Por favor selecciona un tema primero.')
|
||||
return
|
||||
}
|
||||
setChunkFileName(filename)
|
||||
setChunkFileTema(tema)
|
||||
setChunkViewerOpen(true)
|
||||
}
|
||||
|
||||
// Handlers para chunking
|
||||
const handleStartChunking = (filename: string, tema: string) => {
|
||||
if (!tema) {
|
||||
alert('No hay tema seleccionado. Por favor selecciona un tema primero.')
|
||||
return
|
||||
}
|
||||
setChunkingFileName(filename)
|
||||
setChunkingFileTema(tema)
|
||||
setChunkingCollectionName(tema) // Usar el tema como nombre de colección
|
||||
setChunkingConfigOpen(true)
|
||||
}
|
||||
|
||||
const handlePreviewChunking = (config: ChunkingConfig) => {
|
||||
setChunkingConfig(config)
|
||||
setChunkingConfigOpen(false)
|
||||
setChunkPreviewOpen(true)
|
||||
}
|
||||
|
||||
const handleAcceptChunking = async (config: ChunkingConfig) => {
|
||||
try {
|
||||
const result = await api.processChunkingFull(config)
|
||||
alert(`Procesamiento completado: ${result.chunks_added} chunks agregados a ${result.collection_name}`)
|
||||
// Recargar archivos para actualizar el estado
|
||||
loadFiles()
|
||||
} catch (error) {
|
||||
console.error('Error processing PDF:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelChunking = () => {
|
||||
setChunkPreviewOpen(false)
|
||||
setChunkingConfig(null)
|
||||
// Opcionalmente volver al modal de configuración
|
||||
// setChunkingConfigOpen(true)
|
||||
}
|
||||
|
||||
const filteredFiles = files.filter(file =>
|
||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
@@ -350,10 +417,19 @@ export function Dashboard() {
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Procesar con chunking"
|
||||
onClick={() => handleStartChunking(file.name, file.tema)}
|
||||
>
|
||||
<Scissors className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Ver chunks"
|
||||
onClick={() => handleViewChunks(file.name, file.tema)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -406,6 +482,41 @@ export function Dashboard() {
|
||||
fileName={previewFileName}
|
||||
onDownload={handleDownloadFromPreview}
|
||||
/>
|
||||
|
||||
{/* Collection Verifier - Verifica/crea colección cuando se selecciona un tema */}
|
||||
<CollectionVerifier
|
||||
tema={selectedTema}
|
||||
onVerified={(exists) => {
|
||||
console.log(`Collection ${selectedTema} exists: ${exists}`)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Chunk Viewer Modal */}
|
||||
<ChunkViewerModal
|
||||
isOpen={chunkViewerOpen}
|
||||
onClose={() => setChunkViewerOpen(false)}
|
||||
fileName={chunkFileName}
|
||||
tema={chunkFileTema}
|
||||
/>
|
||||
|
||||
{/* Modal de configuración de chunking */}
|
||||
<ChunkingConfigModal
|
||||
isOpen={chunkingConfigOpen}
|
||||
onClose={() => setChunkingConfigOpen(false)}
|
||||
fileName={chunkingFileName}
|
||||
tema={chunkingFileTema}
|
||||
collectionName={chunkingCollectionName}
|
||||
onPreview={handlePreviewChunking}
|
||||
/>
|
||||
|
||||
{/* Panel de preview de chunks */}
|
||||
<ChunkPreviewPanel
|
||||
isOpen={chunkPreviewOpen}
|
||||
onClose={() => setChunkPreviewOpen(false)}
|
||||
config={chunkingConfig}
|
||||
onAccept={handleAcceptChunking}
|
||||
onCancel={handleCancelChunking}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
159
frontend/src/components/ui/select.tsx
Normal file
159
frontend/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 origin-[--radix-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
27
frontend/src/components/ui/switch.tsx
Normal file
27
frontend/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
53
frontend/src/components/ui/tabs.tsx
Normal file
53
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
22
frontend/src/components/ui/textarea.tsx
Normal file
22
frontend/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
@@ -188,4 +188,236 @@ export const api = {
|
||||
return data.url
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Vector Database / Qdrant Operations
|
||||
// ============================================================================
|
||||
|
||||
// Health check de la base de datos vectorial
|
||||
vectorHealthCheck: async (): Promise<{ status: string; db_type: string; message: string }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/health`)
|
||||
if (!response.ok) throw new Error('Error checking vector DB health')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Verificar si una colección existe
|
||||
checkCollectionExists: async (collectionName: string): Promise<{ exists: boolean; collection_name: string }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/collections/exists`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ collection_name: collectionName }),
|
||||
})
|
||||
if (!response.ok) throw new Error('Error checking collection')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Crear una nueva colección
|
||||
createCollection: async (
|
||||
collectionName: string,
|
||||
vectorSize: number = 3072,
|
||||
distance: string = 'Cosine'
|
||||
): Promise<{ success: boolean; collection_name: string; message: string }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/collections/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collectionName,
|
||||
vector_size: vectorSize,
|
||||
distance: distance,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || 'Error creating collection')
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Eliminar una colección
|
||||
deleteCollection: async (collectionName: string): Promise<{ success: boolean; collection_name: string; message: string }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/collections/${encodeURIComponent(collectionName)}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) throw new Error('Error deleting collection')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Obtener información de una colección
|
||||
getCollectionInfo: async (collectionName: string): Promise<{
|
||||
name: string
|
||||
vectors_count: number
|
||||
vectors_config: { size: number; distance: string }
|
||||
status: string
|
||||
}> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/collections/${encodeURIComponent(collectionName)}/info`)
|
||||
if (!response.ok) throw new Error('Error getting collection info')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Verificar si un archivo existe en una colección
|
||||
checkFileExistsInCollection: async (
|
||||
collectionName: string,
|
||||
fileName: string
|
||||
): Promise<{ exists: boolean; collection_name: string; file_name: string; chunk_count?: number }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/files/exists`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collectionName,
|
||||
file_name: fileName,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Error checking file in collection')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Obtener chunks de un archivo
|
||||
getChunksByFile: async (
|
||||
collectionName: string,
|
||||
fileName: string,
|
||||
limit?: number
|
||||
): Promise<{
|
||||
collection_name: string
|
||||
file_name: string
|
||||
chunks: Array<{ id: string; payload: any; vector?: number[] }>
|
||||
total_chunks: number
|
||||
}> => {
|
||||
const url = limit
|
||||
? `${API_BASE_URL}/vectors/collections/${encodeURIComponent(collectionName)}/files/${encodeURIComponent(fileName)}/chunks?limit=${limit}`
|
||||
: `${API_BASE_URL}/vectors/collections/${encodeURIComponent(collectionName)}/files/${encodeURIComponent(fileName)}/chunks`
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error('Error getting chunks')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Eliminar archivo de colección
|
||||
deleteFileFromCollection: async (
|
||||
collectionName: string,
|
||||
fileName: string
|
||||
): Promise<{ success: boolean; collection_name: string; file_name: string; chunks_deleted: number; message: string }> => {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/vectors/collections/${encodeURIComponent(collectionName)}/files/${encodeURIComponent(fileName)}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
if (!response.ok) throw new Error('Error deleting file from collection')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Agregar chunks a una colección
|
||||
addChunks: async (
|
||||
collectionName: string,
|
||||
chunks: Array<{ id: string; vector: number[]; payload: any }>
|
||||
): Promise<{ success: boolean; collection_name: string; chunks_added: number; message: string }> => {
|
||||
const response = await fetch(`${API_BASE_URL}/vectors/chunks/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
collection_name: collectionName,
|
||||
chunks: chunks,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Error adding chunks')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Chunking Operations
|
||||
// ============================================================================
|
||||
|
||||
// Obtener perfiles de chunking predefinidos
|
||||
getChunkingProfiles: async (): Promise<{
|
||||
profiles: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
max_tokens: number
|
||||
target_tokens: number
|
||||
chunk_size: number
|
||||
chunk_overlap: number
|
||||
use_llm: boolean
|
||||
}>
|
||||
}> => {
|
||||
const response = await fetch(`${API_BASE_URL}/chunking/profiles`)
|
||||
if (!response.ok) throw new Error('Error fetching chunking profiles')
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Generar preview de chunks (hasta 3 chunks)
|
||||
generateChunkPreview: async (config: {
|
||||
file_name: string
|
||||
tema: string
|
||||
max_tokens?: number
|
||||
target_tokens?: number
|
||||
chunk_size?: number
|
||||
chunk_overlap?: number
|
||||
use_llm?: boolean
|
||||
custom_instructions?: string
|
||||
}): Promise<{
|
||||
success: boolean
|
||||
file_name: string
|
||||
tema: string
|
||||
chunks: Array<{
|
||||
index: number
|
||||
text: string
|
||||
page: number
|
||||
file_name: string
|
||||
tokens: number
|
||||
}>
|
||||
message: string
|
||||
}> => {
|
||||
const response = await fetch(`${API_BASE_URL}/chunking/preview`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || 'Error generating preview')
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Procesar PDF completo
|
||||
processChunkingFull: async (config: {
|
||||
file_name: string
|
||||
tema: string
|
||||
collection_name: string
|
||||
max_tokens?: number
|
||||
target_tokens?: number
|
||||
chunk_size?: number
|
||||
chunk_overlap?: number
|
||||
use_llm?: boolean
|
||||
custom_instructions?: string
|
||||
}): Promise<{
|
||||
success: boolean
|
||||
collection_name: string
|
||||
file_name: string
|
||||
total_chunks: number
|
||||
chunks_added: number
|
||||
message: string
|
||||
}> => {
|
||||
const response = await fetch(`${API_BASE_URL}/chunking/process`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || 'Error processing PDF')
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user