Merge branch 'main' into refine/mcp-tool
@@ -10,7 +10,7 @@ import Add from './assets/add.svg';
|
||||
import DocsGPT3 from './assets/cute_docsgpt3.svg';
|
||||
import Discord from './assets/discord.svg';
|
||||
import Expand from './assets/expand.svg';
|
||||
import Github from './assets/github.svg';
|
||||
import Github from './assets/git_nav.svg';
|
||||
import Hamburger from './assets/hamburger.svg';
|
||||
import openNewChat from './assets/openNewChat.svg';
|
||||
import Pin from './assets/pin.svg';
|
||||
@@ -568,6 +568,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
>
|
||||
<img
|
||||
src={Discord}
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Join Discord community"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
/>
|
||||
@@ -581,8 +583,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
>
|
||||
<img
|
||||
src={Twitter}
|
||||
width={20}
|
||||
height={20}
|
||||
alt="Follow us on Twitter"
|
||||
className="m-2 w-5 self-center filter dark:invert"
|
||||
className="m-2 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
@@ -595,7 +599,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
<img
|
||||
src={Github}
|
||||
alt="View on GitHub"
|
||||
className="m-2 w-6 self-center filter dark:invert"
|
||||
width={28}
|
||||
height={28}
|
||||
className="m-2 self-center filter dark:invert"
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
3
frontend/src/assets/crawler.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.2891 15.81L21.7091 14.39L18.4991 11.21L15.4991 10.36L17.4091 10.1L21.5991 6.89999L20.3991 5.29998L16.5891 8.14999L13.9091 8.59999L17.1091 5.40999L15.9991 0.859985L13.9991 1.33999L14.8591 4.78999L13.7591 5.92999C13.5285 5.38882 13.144 4.92736 12.6533 4.60302C12.1625 4.27867 11.5873 4.10574 10.9991 4.10574C10.4108 4.10574 9.83559 4.27867 9.34487 4.60302C8.85414 4.92736 8.4696 5.38882 8.23906 5.92999L7.10906 4.78999L7.99906 1.33999L5.99906 0.859985L4.88906 5.40999L8.08906 8.59999L5.39906 8.14999L1.59906 5.29998L0.399063 6.89999L4.59906 10.1L6.45906 10.41L3.45906 11.26L0.289062 14.39L1.70906 15.81L4.49906 12.99L6.86906 12.32L2.99906 15.64V21.1H4.99906V16.56L6.55906 15.22C6.73264 16.2723 7.27432 17.2287 8.08751 17.9188C8.90071 18.6088 9.93255 18.9876 10.9991 18.9876C12.0656 18.9876 13.0974 18.6088 13.9106 17.9188C14.7238 17.2287 15.2655 16.2723 15.4391 15.22L16.9991 16.56V21.1H18.9991V15.64L15.1291 12.32L17.4991 12.99L20.2891 15.81Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
frontend/src/assets/drive.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.01 0.784912C9.928 0.784912 8.256 0.804912 8.267 0.831912C8.277 0.851912 9.975 3.83291 12.041 7.45191L15.801 14.0259H19.561C21.642 14.0259 23.314 14.0059 23.303 13.9789C23.298 13.9589 21.595 10.9779 19.528 7.35891L15.768 0.784912H12.01ZM7.25 2.51491C6.03029 4.61565 4.82028 6.72201 3.62 8.83391L0 15.1679L1.89 18.4659L3.775 21.7629L7.395 15.4279L11.013 9.09791L9.133 5.81091C8.1 4.00391 7.255 2.52091 7.25 2.51491ZM9.509 15.1679L9.306 15.5159C9.192 15.7139 8.346 17.1879 7.426 18.8029C6.864 19.7952 6.29799 20.7852 5.728 21.7729C5.718 21.7989 8.968 21.8149 12.95 21.8149H20.194L21.99 18.6579C22.982 16.9239 23.84 15.4279 23.896 15.3349L24 15.1679H16.751H9.509Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 792 B |
@@ -1,3 +1,10 @@
|
||||
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 26.0003H18C19.1 26.0003 20 25.1003 20 24.0003V14.0003H23.18C24.96 14.0003 25.86 11.8403 24.6 10.5803L15.42 1.40032C15.235 1.21491 15.0152 1.06782 14.7732 0.967453C14.5313 0.86709 14.2719 0.81543 14.01 0.81543C13.7481 0.81543 13.4887 0.86709 13.2468 0.967453C13.0048 1.06782 12.785 1.21491 12.6 1.40032L3.42 10.5803C2.16 11.8403 3.04 14.0003 4.82 14.0003H8V24.0003C8 25.1003 8.9 26.0003 10 26.0003ZM2 30.0003H26C27.1 30.0003 28 30.9003 28 32.0003C28 33.1003 27.1 34.0003 26 34.0003H2C0.9 34.0003 0 33.1003 0 32.0003C0 30.9003 0.9 30.0003 2 30.0003Z" fill="#949494"/>
|
||||
</svg>
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_9890_21170)">
|
||||
<path d="M12.75 19.1V8.91248L9.5 12.1625L7.75 10.35L14 4.09998L20.25 10.35L18.5 12.1625L15.25 8.91248V19.1H12.75ZM6.5 24.1C5.8125 24.1 5.22417 23.8554 4.735 23.3662C4.24583 22.8771 4.00083 22.2883 4 21.6V17.85H6.5V21.6H21.5V17.85H24V21.6C24 22.2875 23.7554 22.8762 23.2663 23.3662C22.7771 23.8562 22.1883 24.1008 21.5 24.1H6.5Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_9890_21170">
|
||||
<rect width="24" height="24" fill="white" transform="translate(0 0.0999756)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 681 B After Width: | Height: | Size: 630 B |
3
frontend/src/assets/git_nav.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.8175 3.09139C20.0845 2.34392 19.2025 1.9707 18.1705 1.9707H5.6835C4.6515 1.9707 3.7695 2.34392 3.0365 3.09139C2.3035 3.83885 1.9375 4.73825 1.9375 5.79061V18.524C1.9375 19.5763 2.3035 20.4757 3.0365 21.2232C3.7695 21.9707 4.6515 22.3439 5.6835 22.3439H8.5975C8.7875 22.3439 8.9305 22.3368 9.0265 22.3235C9.13819 22.3007 9.23901 22.2399 9.3125 22.1512C9.4075 22.0492 9.4555 21.9013 9.4555 21.7076L9.4485 20.8051C9.4445 20.23 9.4425 19.7752 9.4425 19.4387L9.1425 19.4917C8.9525 19.5274 8.7125 19.5427 8.4215 19.5386C8.11819 19.5329 7.81584 19.5019 7.5175 19.4458C7.1999 19.386 6.90093 19.2497 6.6455 19.0481C6.37799 18.8418 6.17847 18.5572 6.0735 18.2323L5.9435 17.9264C5.83393 17.6851 5.69627 17.4581 5.5335 17.2503C5.3475 17.0025 5.1585 16.8353 4.9675 16.7466L4.8775 16.6803C4.81474 16.6345 4.75766 16.5811 4.7075 16.5212C4.65959 16.4657 4.62015 16.4031 4.5905 16.3356C4.5645 16.2734 4.5865 16.2224 4.6555 16.1827C4.7255 16.1419 4.8505 16.1225 5.0335 16.1225L5.2935 16.1633C5.4665 16.198 5.6815 16.304 5.9365 16.4804C6.19456 16.6598 6.41013 16.8957 6.5675 17.1708C6.7675 17.5328 7.0075 17.8091 7.2895 17.9998C7.5715 18.1895 7.8555 18.2854 8.1415 18.2854C8.4275 18.2854 8.6745 18.2629 8.8835 18.2191C9.08561 18.1765 9.28201 18.1094 9.4685 18.0192C9.5465 17.4278 9.7585 16.9709 10.1055 16.6528C9.65588 16.6078 9.21026 16.5281 8.7725 16.4142C8.34529 16.2945 7.93444 16.1208 7.5495 15.8972C7.14675 15.6736 6.79101 15.3714 6.5025 15.008C6.2255 14.6541 5.9975 14.1901 5.8195 13.616C5.6425 13.0409 5.5535 12.377 5.5535 11.6255C5.5535 10.5558 5.8955 9.64519 6.5805 8.89263C6.2605 8.08908 6.2905 7.18662 6.6715 6.18831C6.9235 6.10775 7.2965 6.16791 7.7905 6.36676C8.2845 6.56561 8.6465 6.7359 8.8765 6.87662C9.1065 7.01939 9.2905 7.13869 9.4295 7.23557C10.2425 7.00486 11.0826 6.88889 11.9265 6.8909C12.7855 6.8909 13.6175 7.00613 14.4245 7.23557L14.9185 6.91741C15.2985 6.68476 15.6993 6.48946 16.1155 6.33413C16.5755 6.15669 16.9255 6.10877 17.1695 6.18831C17.5595 7.18764 17.5935 8.08908 17.2725 8.89365C17.9575 9.64519 18.3005 10.5558 18.3005 11.6265C18.3005 12.3781 18.2115 13.044 18.0335 13.6221C17.8565 14.2013 17.6265 14.6653 17.3445 15.0151C17.0509 15.3739 16.6937 15.6731 16.2915 15.8972C15.8715 16.1358 15.4635 16.3081 15.0685 16.4142C14.6308 16.5284 14.1852 16.6085 13.7355 16.6538C14.1855 17.0515 14.4115 17.6786 14.4115 18.5362V21.7076C14.4115 21.8575 14.4325 21.9788 14.4765 22.0716C14.4967 22.1163 14.5256 22.1564 14.5613 22.1895C14.597 22.2226 14.6389 22.2481 14.6845 22.2643C14.7805 22.299 14.8645 22.3215 14.9385 22.3296C15.0125 22.3398 15.1185 22.3429 15.2565 22.3429H18.1705C19.2025 22.3429 20.0845 21.9696 20.8175 21.2222C21.5495 20.4757 21.9165 19.5753 21.9165 18.523V5.79061C21.9165 4.73825 21.5505 3.83885 20.8175 3.09139Z" fill="#747474"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,5 +1,3 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>github</title>
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
<path d="M12,2A10,10,0,0,0,8.84,21.5c.5.08.66-.23.66-.5V19.31C6.73,19.91,6.14,18,6.14,18A2.69,2.69,0,0,0,5,16.5c-.91-.62.07-.6.07-.6a2.1,2.1,0,0,1,1.53,1,2.15,2.15,0,0,0,2.91.83,2.16,2.16,0,0,1,.63-1.34C8,16.17,5.62,15.31,5.62,11.5a3.87,3.87,0,0,1,1-2.71,3.58,3.58,0,0,1,.1-2.64s.84-.27,2.75,1a9.63,9.63,0,0,1,5,0c1.91-1.29,2.75-1,2.75-1a3.58,3.58,0,0,1,.1,2.64,3.87,3.87,0,0,1,1,2.71c0,3.82-2.34,4.66-4.57,4.91a2.39,2.39,0,0,1,.69,1.85V21c0,.27.16.59.67.5A10,10,0,0,0,12,2Z" fill="black" fill-opacity="0.54"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 0.299927C8.68678 0.299927 7.38642 0.558584 6.17317 1.06113C4.95991 1.56368 3.85752 2.30027 2.92893 3.22886C1.05357 5.10422 0 7.64776 0 10.2999C0 14.7199 2.87 18.4699 6.84 19.7999C7.34 19.8799 7.5 19.5699 7.5 19.2999V17.6099C4.73 18.2099 4.14 16.2699 4.14 16.2699C3.68 15.1099 3.03 14.7999 3.03 14.7999C2.12 14.1799 3.1 14.1999 3.1 14.1999C4.1 14.2699 4.63 15.2299 4.63 15.2299C5.5 16.7499 6.97 16.2999 7.54 16.0599C7.63 15.4099 7.89 14.9699 8.17 14.7199C5.95 14.4699 3.62 13.6099 3.62 9.79993C3.62 8.68993 4 7.79993 4.65 7.08993C4.55 6.83993 4.2 5.79993 4.75 4.44993C4.75 4.44993 5.59 4.17993 7.5 5.46993C8.29 5.24993 9.15 5.13993 10 5.13993C10.85 5.13993 11.71 5.24993 12.5 5.46993C14.41 4.17993 15.25 4.44993 15.25 4.44993C15.8 5.79993 15.45 6.83993 15.35 7.08993C16 7.79993 16.38 8.68993 16.38 9.79993C16.38 13.6199 14.04 14.4599 11.81 14.7099C12.17 15.0199 12.5 15.6299 12.5 16.5599V19.2999C12.5 19.5699 12.66 19.8899 13.17 19.7999C17.14 18.4599 20 14.7199 20 10.2999C20 8.98671 19.7413 7.68635 19.2388 6.47309C18.7362 5.25984 17.9997 4.15744 17.0711 3.22886C16.1425 2.30027 15.0401 1.56368 13.8268 1.06113C12.6136 0.558584 11.3132 0.299927 10 0.299927Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 1.3 KiB |
4
frontend/src/assets/reddit.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.7519 13.3399C10.7519 12.7699 10.2819 12.2999 9.71187 12.2999C9.14187 12.2999 8.67188 12.7699 8.67188 13.3399C8.67188 13.6158 8.78145 13.8803 8.97648 14.0753C9.17152 14.2704 9.43605 14.3799 9.71187 14.3799C9.9877 14.3799 10.2522 14.2704 10.4473 14.0753C10.6423 13.8803 10.7519 13.6158 10.7519 13.3399ZM14.0919 15.7099C13.6419 16.1599 12.6819 16.3199 12.0019 16.3199C11.3219 16.3199 10.3619 16.1599 9.91187 15.7099C9.88755 15.6839 9.85813 15.6631 9.82545 15.6489C9.79276 15.6347 9.75751 15.6274 9.72188 15.6274C9.68624 15.6274 9.65099 15.6347 9.6183 15.6489C9.58562 15.6631 9.5562 15.6839 9.53187 15.7099C9.50583 15.7343 9.48507 15.7637 9.47088 15.7964C9.45668 15.829 9.44936 15.8643 9.44936 15.8999C9.44936 15.9356 9.45668 15.9708 9.47088 16.0035C9.48507 16.0362 9.50583 16.0656 9.53187 16.0899C10.2419 16.7999 11.6019 16.8599 12.0019 16.8599C12.4019 16.8599 13.7619 16.7999 14.4719 16.0899C14.4979 16.0656 14.5187 16.0362 14.5329 16.0035C14.5471 15.9708 14.5544 15.9356 14.5544 15.8999C14.5544 15.8643 14.5471 15.829 14.5329 15.7964C14.5187 15.7637 14.4979 15.7343 14.4719 15.7099C14.3719 15.6099 14.2019 15.6099 14.0919 15.7099ZM14.2919 12.2999C13.7219 12.2999 13.2519 12.7699 13.2519 13.3399C13.2519 13.9099 13.7219 14.3799 14.2919 14.3799C14.8619 14.3799 15.3319 13.9099 15.3319 13.3399C15.3319 12.7699 14.8719 12.2999 14.2919 12.2999Z" fill="black"/>
|
||||
<path d="M12 2.29993C6.48 2.29993 2 6.77993 2 12.2999C2 17.8199 6.48 22.2999 12 22.2999C17.52 22.2999 22 17.8199 22 12.2999C22 6.77993 17.52 2.29993 12 2.29993ZM17.8 13.6299C17.82 13.7699 17.83 13.9199 17.83 14.0699C17.83 16.3099 15.22 18.1299 12 18.1299C8.78 18.1299 6.17 16.3099 6.17 14.0699C6.17 13.9199 6.18 13.7699 6.2 13.6299C5.69 13.3999 5.34 12.8899 5.34 12.2999C5.33852 12.0132 5.4218 11.7324 5.57939 11.4928C5.73698 11.2532 5.96185 11.0656 6.22576 10.9534C6.48966 10.8412 6.78083 10.8095 7.06269 10.8622C7.34456 10.915 7.60454 11.0499 7.81 11.2499C8.82 10.5199 10.22 10.0599 11.77 10.0099L12.51 6.51993C12.52 6.44993 12.56 6.38993 12.62 6.35993C12.68 6.31993 12.75 6.30993 12.82 6.31993L15.24 6.83993C15.3221 6.67351 15.4472 6.53207 15.6023 6.4303C15.7575 6.32853 15.9371 6.27013 16.1224 6.26115C16.3077 6.25217 16.4921 6.29294 16.6564 6.37924C16.8207 6.46553 16.9589 6.59421 17.0566 6.75191C17.1544 6.90962 17.2082 7.09062 17.2125 7.27613C17.2167 7.46164 17.1712 7.64491 17.0808 7.80692C16.9903 7.96894 16.8582 8.1038 16.698 8.19753C16.5379 8.29125 16.3556 8.34042 16.17 8.33993C15.61 8.33993 15.16 7.89993 15.13 7.34993L12.96 6.88993L12.3 10.0099C13.83 10.0599 15.2 10.5299 16.2 11.2499C16.3533 11.1035 16.5367 10.9924 16.7375 10.9243C16.9382 10.8562 17.1514 10.8328 17.3621 10.8557C17.5728 10.8787 17.776 10.9473 17.9574 11.057C18.1388 11.1666 18.2941 11.3145 18.4123 11.4905C18.5306 11.6664 18.609 11.866 18.642 12.0754C18.6751 12.2847 18.662 12.4988 18.6037 12.7026C18.5454 12.9064 18.4432 13.0949 18.3044 13.2551C18.1656 13.4153 17.9934 13.5432 17.8 13.6299Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
3
frontend/src/assets/url.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="23" viewBox="0 0 22 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.304 8.62401L19.486 11.806C20.0023 12.3155 20.4128 12.922 20.6937 13.5908C20.9747 14.2596 21.1206 14.9773 21.123 15.7026C21.1254 16.428 20.9843 17.1467 20.7079 17.8173C20.4314 18.4879 20.025 19.0972 19.5121 19.6102C18.9992 20.1231 18.3899 20.5295 17.7193 20.8059C17.0486 21.0824 16.33 21.2235 15.6046 21.221C14.8792 21.2186 14.1615 21.0727 13.4928 20.7918C12.824 20.5108 12.2174 20.1003 11.708 19.584L10.648 18.524C10.5046 18.3857 10.3903 18.2202 10.3116 18.0373C10.2329 17.8543 10.1914 17.6575 10.1896 17.4583C10.1878 17.2592 10.2256 17.0616 10.301 16.8772C10.3763 16.6929 10.4876 16.5253 10.6284 16.3844C10.7691 16.2435 10.9366 16.1321 11.1209 16.0566C11.3052 15.9811 11.5027 15.943 11.7019 15.9446C11.901 15.9463 12.0979 15.9876 12.2809 16.0661C12.464 16.1446 12.6295 16.2588 12.768 16.402L13.83 17.463C14.2996 17.9284 14.9344 18.1888 15.5955 18.1873C16.2567 18.1857 16.8903 17.9224 17.3577 17.4548C17.8252 16.9872 18.0884 16.3535 18.0897 15.6924C18.0911 15.0312 17.8305 14.3965 17.365 13.927L14.183 10.745C13.839 10.4009 13.4022 10.1647 12.926 10.0652C12.4498 9.96574 11.9549 10.0074 11.502 10.185C11.3406 10.249 11.1893 10.3143 11.048 10.381L10.584 10.598C9.96396 10.878 9.48696 10.998 8.87996 10.392C8.00796 9.52001 8.23396 8.71501 9.29696 7.98201C10.3559 7.25337 11.6365 6.91862 12.9166 7.0359C14.1966 7.15318 15.3951 7.71508 16.304 8.62401ZM10.294 2.61401L11.354 3.67401C11.6273 3.95678 11.7787 4.33562 11.7755 4.72891C11.7722 5.12221 11.6147 5.49851 11.3367 5.77675C11.0587 6.055 10.6826 6.21293 10.2893 6.21653C9.89597 6.22013 9.517 6.06912 9.23396 5.79601L8.17296 4.73601C7.94241 4.49717 7.66661 4.30664 7.36163 4.17553C7.05666 4.04442 6.72863 3.97536 6.39668 3.97239C6.06474 3.96941 5.73552 4.03257 5.42824 4.15818C5.12097 4.2838 4.84179 4.46935 4.60699 4.70402C4.37219 4.93868 4.18648 5.21776 4.06069 5.52496C3.9349 5.83217 3.87155 6.16135 3.87434 6.4933C3.87713 6.82525 3.94601 7.15332 4.07694 7.45836C4.20788 7.76341 4.39825 8.03933 4.63696 8.27001L7.81896 11.452C8.16289 11.7961 8.59974 12.0323 9.07595 12.1318C9.55217 12.2313 10.0471 12.1896 10.5 12.012C10.6613 11.948 10.8126 11.8827 10.954 11.816L11.418 11.599C12.038 11.319 12.516 11.199 13.122 11.805C13.994 12.677 13.768 13.482 12.705 14.215C11.6461 14.9437 10.3654 15.2784 9.08537 15.1611C7.80535 15.0438 6.60683 14.4819 5.69796 13.573L2.51596 10.391C1.99962 9.88154 1.58916 9.27497 1.30821 8.60622C1.02726 7.93747 0.881367 7.21975 0.878937 6.49438C0.876507 5.76901 1.01759 5.05033 1.29405 4.37971C1.57052 3.70909 1.9769 3.09978 2.48982 2.58686C3.00273 2.07395 3.61204 1.66756 4.28266 1.3911C4.95328 1.11463 5.67196 0.973553 6.39733 0.975983C7.1227 0.978413 7.84042 1.1243 8.50917 1.40526C9.17793 1.68621 9.7845 2.09767 10.294 2.61401Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,5 +1,6 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
|
||||
interface ConnectorAuthProps {
|
||||
@@ -7,22 +8,24 @@ interface ConnectorAuthProps {
|
||||
onSuccess: (data: { session_token: string; user_email: string }) => void;
|
||||
onError: (error: string) => void;
|
||||
label?: string;
|
||||
isConnected?: boolean;
|
||||
userEmail?: string;
|
||||
onDisconnect?: () => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const providerLabel = (provider: string) => {
|
||||
const map: Record<string, string> = {
|
||||
google_drive: 'Google Drive',
|
||||
};
|
||||
return map[provider] || provider.replace(/_/g, ' ');
|
||||
};
|
||||
|
||||
const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
provider,
|
||||
onSuccess,
|
||||
onError,
|
||||
label,
|
||||
isConnected = false,
|
||||
userEmail = '',
|
||||
onDisconnect,
|
||||
errorMessage,
|
||||
}) => {
|
||||
const token = useSelector(selectToken);
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const completedRef = useRef(false);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
@@ -36,12 +39,8 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
|
||||
const handleAuthMessage = (event: MessageEvent) => {
|
||||
const successGeneric = event.data?.type === 'connector_auth_success';
|
||||
const successProvider =
|
||||
event.data?.type === `${provider}_auth_success` ||
|
||||
event.data?.type === 'google_drive_auth_success';
|
||||
const errorProvider =
|
||||
event.data?.type === `${provider}_auth_error` ||
|
||||
event.data?.type === 'google_drive_auth_error';
|
||||
const successProvider = event.data?.type === `${provider}_auth_success`;
|
||||
const errorProvider = event.data?.type === `${provider}_auth_error`;
|
||||
|
||||
if (successGeneric || successProvider) {
|
||||
completedRef.current = true;
|
||||
@@ -109,22 +108,58 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const buttonLabel = label || `Connect ${providerLabel(provider)}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleAuth}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-500 px-4 py-3 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.28 3l5.72 10H24l-5.72-10H6.28zm11.44 0L12 13l5.72 10H24L18.28 3h-.56zM0 13l5.72 10h5.72L5.72 13H0z"
|
||||
/>
|
||||
</svg>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
<>
|
||||
{errorMessage && (
|
||||
<div className="mb-4 flex items-center gap-2 rounded-lg border border-[#E60000] dark:border-[#D42626] bg-transparent dark:bg-[#D426261A] p-2">
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.09974 24.5422H22.9C24.5156 24.5422 25.5228 22.7901 24.715 21.3947L16.8149 7.74526C16.007 6.34989 13.9927 6.34989 13.1848 7.74526L5.28471 21.3947C4.47686 22.7901 5.48405 24.5422 7.09974 24.5422ZM14.9998 17.1981C14.4228 17.1981 13.9507 16.726 13.9507 16.149V14.0507C13.9507 13.4736 14.4228 13.0015 14.9998 13.0015C15.5769 13.0015 16.049 13.4736 16.049 14.0507V16.149C16.049 16.726 15.5769 17.1981 14.9998 17.1981ZM16.049 21.3947H13.9507V19.2964H16.049V21.3947Z" fill={isDarkTheme ? '#EECF56' : '#E60000'} />
|
||||
</svg>
|
||||
|
||||
<span className='text-[#E60000] dark:text-[#E37064] text-sm' style={{
|
||||
fontFamily: 'Inter',
|
||||
lineHeight: '100%'
|
||||
}}>
|
||||
{errorMessage}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected ? (
|
||||
<div className="mb-4">
|
||||
<div className="w-full flex items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-[#212121] font-medium text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
<span>Connected as {userEmail}</span>
|
||||
</div>
|
||||
{onDisconnect && (
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="text-[#212121] hover:text-gray-700 font-medium text-xs underline"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleAuth}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-500 px-4 py-3 text-white transition-colors hover:bg-blue-600"
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.28 3l5.72 10H24l-5.72-10H6.28zm11.44 0L12 13l5.72 10H24L18.28 3h-.56zM0 13l5.72 10h5.72L5.72 13H0z"
|
||||
/>
|
||||
</svg>
|
||||
{label}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorAuth;
|
||||
export default ConnectorAuth;
|
||||
@@ -3,8 +3,10 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { formatBytes } from '../utils/stringUtils';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Chunks from './Chunks';
|
||||
import ContextMenu, { MenuOption } from './ContextMenu';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import userService from '../api/services/userService';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
import FolderIcon from '../assets/folder.svg';
|
||||
@@ -12,7 +14,17 @@ import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import ThreeDots from '../assets/three-dots.svg';
|
||||
import EyeView from '../assets/eye-view.svg';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
import CheckmarkIcon from '../assets/checkMark2.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface FileNode {
|
||||
type?: string;
|
||||
@@ -64,6 +76,7 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const [syncProgress, setSyncProgress] = useState<number>(0);
|
||||
const [sourceProvider, setSourceProvider] = useState<string>('');
|
||||
const [syncDone, setSyncDone] = useState<boolean>(false);
|
||||
const [syncConfirmationModal, setSyncConfirmationModal] = useState<ActiveState>('INACTIVE');
|
||||
|
||||
useOutsideAlerter(
|
||||
searchDropdownRef,
|
||||
@@ -343,7 +356,7 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
|
||||
{/* Sync button */}
|
||||
<button
|
||||
onClick={handleSync}
|
||||
onClick={() => setSyncConfirmationModal('ACTIVE')}
|
||||
disabled={isSyncing}
|
||||
className={`flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap transition-colors ${
|
||||
isSyncing
|
||||
@@ -359,7 +372,7 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={SyncIcon}
|
||||
src={syncDone ? CheckmarkIcon : SyncIcon}
|
||||
alt={t('settings.sources.sync')}
|
||||
className={`mr-2 h-4 w-4 brightness-0 invert filter ${isSyncing ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
@@ -374,39 +387,36 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileTree = (directory: DirectoryStructure) => {
|
||||
if (!directory) return [];
|
||||
|
||||
const renderFileTree = (directory: DirectoryStructure): React.ReactNode[] => {
|
||||
// Create parent directory row
|
||||
const parentRow =
|
||||
currentPath.length > 0
|
||||
? [
|
||||
<tr
|
||||
key="parent-dir"
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4"></td>
|
||||
</tr>,
|
||||
]
|
||||
<TableRow
|
||||
key="parent-dir"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right"></TableCell>
|
||||
</TableRow>,
|
||||
]
|
||||
: [];
|
||||
|
||||
// Sort entries: directories first, then files, both alphabetically
|
||||
@@ -434,36 +444,35 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const dirStats = calculateDirectoryStats(node as DirectoryStructure);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => navigateToDirectory(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.folderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{dirStats.totalTokens > 0
|
||||
? dirStats.totalTokens.toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
|
||||
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E] font-medium"
|
||||
aria-label={t('settings.sources.menuAlt')}
|
||||
>
|
||||
<img
|
||||
@@ -483,8 +492,8 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -496,30 +505,29 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
const menuRef = getMenuRef(itemId);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => handleFileClick(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FileIcon}
|
||||
alt={t('settings.sources.fileAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm md:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="left">
|
||||
{node.size_bytes ? formatBytes(node.size_bytes) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -543,8 +551,8 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -702,28 +710,45 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
|
||||
<div className="mb-2">{renderPathNavigation()}</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<table className="w-full min-w-[600px] table-auto bg-transparent">
|
||||
<thead className="bg-gray-100 dark:bg-[#27282D]">
|
||||
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<th className="min-w-[200px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40%" align="left">
|
||||
{t('settings.sources.fileName')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="30%" align="left">
|
||||
{t('settings.sources.tokens')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="20%" align="left">
|
||||
{t('settings.sources.size')}
|
||||
</th>
|
||||
<th className="w-10 px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{renderFileTree(getCurrentDirectory())}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableHeader width="10%" align="right">
|
||||
<span className="sr-only">
|
||||
{t('settings.sources.actions')}
|
||||
</span>
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderFileTree(getCurrentDirectory())}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmationModal
|
||||
message={t('settings.sources.syncConfirmation', {
|
||||
sourceName,
|
||||
})}
|
||||
modalState={syncConfirmationModal}
|
||||
setModalState={setSyncConfirmationModal}
|
||||
handleSubmit={handleSync}
|
||||
submitLabel={t('settings.sources.sync')}
|
||||
cancelLabel={t('cancel')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
469
frontend/src/components/FilePicker.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { formatBytes } from '../utils/stringUtils';
|
||||
import { formatDate } from '../utils/dateTimeUtils';
|
||||
import { getSessionToken, setSessionToken, removeSessionToken } from '../utils/providerUtils';
|
||||
import ConnectorAuth from '../components/ConnectorAuth';
|
||||
import FileIcon from '../assets/file.svg';
|
||||
import FolderIcon from '../assets/folder.svg';
|
||||
import CheckIcon from '../assets/checkmark.svg';
|
||||
import SearchIcon from '../assets/search.svg';
|
||||
import Input from './Input';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface CloudFile {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size?: number;
|
||||
modifiedTime: string;
|
||||
isFolder?: boolean;
|
||||
}
|
||||
|
||||
interface CloudFilePickerProps {
|
||||
onSelectionChange: (selectedFileIds: string[], selectedFolderIds?: string[]) => void;
|
||||
onDisconnect?: () => void;
|
||||
provider: string;
|
||||
token: string | null;
|
||||
initialSelectedFiles?: string[];
|
||||
initialSelectedFolders?: string[];
|
||||
}
|
||||
|
||||
export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
onSelectionChange,
|
||||
onDisconnect,
|
||||
provider,
|
||||
token,
|
||||
initialSelectedFiles = [],
|
||||
}) => {
|
||||
const PROVIDER_CONFIG = {
|
||||
google_drive: {
|
||||
displayName: 'Drive',
|
||||
rootName: 'My Drive',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const getProviderConfig = (provider: string) => {
|
||||
return PROVIDER_CONFIG[provider as keyof typeof PROVIDER_CONFIG] || {
|
||||
displayName: provider,
|
||||
rootName: 'Root',
|
||||
};
|
||||
};
|
||||
|
||||
const [files, setFiles] = useState<CloudFile[]>([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState<string[]>(initialSelectedFiles);
|
||||
const [selectedFolders, setSelectedFolders] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasMoreFiles, setHasMoreFiles] = useState(false);
|
||||
const [nextPageToken, setNextPageToken] = useState<string | null>(null);
|
||||
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
|
||||
const [folderPath, setFolderPath] = useState<Array<{ id: string | null, name: string }>>([{
|
||||
id: null,
|
||||
name: getProviderConfig(provider).rootName
|
||||
}]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [authError, setAuthError] = useState<string>('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const isFolder = (file: CloudFile) => {
|
||||
return file.isFolder ||
|
||||
file.type === 'application/vnd.google-apps.folder' ||
|
||||
file.type === 'folder';
|
||||
};
|
||||
|
||||
const loadCloudFiles = useCallback(
|
||||
async (
|
||||
sessionToken: string,
|
||||
folderId: string | null,
|
||||
pageToken?: string,
|
||||
searchQuery: string = ''
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
if (!pageToken) {
|
||||
setFiles([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiHost}/api/connectors/files`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: provider,
|
||||
session_token: sessionToken,
|
||||
folder_id: folderId,
|
||||
limit: 10,
|
||||
page_token: pageToken,
|
||||
search_query: searchQuery
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setFiles(prev => pageToken ? [...prev, ...data.files] : data.files);
|
||||
setNextPageToken(data.next_page_token);
|
||||
setHasMoreFiles(!!data.next_page_token);
|
||||
} else {
|
||||
console.error('Error loading files:', data.error);
|
||||
if (!pageToken) {
|
||||
setFiles([]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading files:', err);
|
||||
if (!pageToken) {
|
||||
setFiles([]);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[token, provider]
|
||||
);
|
||||
|
||||
const validateAndLoadFiles = useCallback(async () => {
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (!sessionToken) {
|
||||
setIsConnected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
const validateResponse = await fetch(`${apiHost}/api/connectors/validate-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ provider: provider, session_token: sessionToken })
|
||||
});
|
||||
|
||||
if (!validateResponse.ok) {
|
||||
removeSessionToken(provider);
|
||||
setIsConnected(false);
|
||||
setAuthError('Session expired. Please reconnect to Google Drive.');
|
||||
return;
|
||||
}
|
||||
|
||||
const validateData = await validateResponse.json();
|
||||
if (validateData.success) {
|
||||
setUserEmail(validateData.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
setFiles([]);
|
||||
setNextPageToken(null);
|
||||
setHasMoreFiles(false);
|
||||
setCurrentFolderId(null);
|
||||
setFolderPath([{
|
||||
id: null, name: getProviderConfig(provider).rootName
|
||||
}]);
|
||||
loadCloudFiles(sessionToken, null, undefined, '');
|
||||
} else {
|
||||
removeSessionToken(provider);
|
||||
setIsConnected(false);
|
||||
setAuthError(validateData.error || 'Session expired. Please reconnect your account.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating session:', error);
|
||||
setAuthError('Failed to validate session. Please reconnect.');
|
||||
setIsConnected(false);
|
||||
}
|
||||
}, [provider, token, loadCloudFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
validateAndLoadFiles();
|
||||
}, [validateAndLoadFiles]);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||
const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
|
||||
if (isNearBottom && hasMoreFiles && !isLoading && nextPageToken) {
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (sessionToken) {
|
||||
loadCloudFiles(sessionToken, currentFolderId, nextPageToken, searchQuery);
|
||||
}
|
||||
}
|
||||
}, [hasMoreFiles, isLoading, nextPageToken, currentFolderId, searchQuery, provider, loadCloudFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
return () => scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
}, [handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (sessionToken) {
|
||||
loadCloudFiles(sessionToken, currentFolderId, undefined, query);
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleFolderClick = (folderId: string, folderName: string) => {
|
||||
if (folderId === currentFolderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
setCurrentFolderId(folderId);
|
||||
setFolderPath(prev => [...prev, { id: folderId, name: folderName }]);
|
||||
setSearchQuery('');
|
||||
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (sessionToken) {
|
||||
loadCloudFiles(sessionToken, folderId, undefined, '');
|
||||
}
|
||||
};
|
||||
|
||||
const navigateBack = (index: number) => {
|
||||
if (index >= folderPath.length - 1) return;
|
||||
|
||||
const newFolderPath = folderPath.slice(0, index + 1);
|
||||
const newFolderId = newFolderPath[newFolderPath.length - 1].id;
|
||||
|
||||
setFolderPath(newFolderPath);
|
||||
setCurrentFolderId(newFolderId);
|
||||
setSearchQuery('');
|
||||
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (sessionToken) {
|
||||
loadCloudFiles(sessionToken, newFolderId, undefined, '');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (fileId: string, isFolder: boolean) => {
|
||||
if (isFolder) {
|
||||
const newSelectedFolders = selectedFolders.includes(fileId)
|
||||
? selectedFolders.filter(id => id !== fileId)
|
||||
: [...selectedFolders, fileId];
|
||||
setSelectedFolders(newSelectedFolders);
|
||||
onSelectionChange(selectedFiles, newSelectedFolders);
|
||||
} else {
|
||||
const newSelectedFiles = selectedFiles.includes(fileId)
|
||||
? selectedFiles.filter(id => id !== fileId)
|
||||
: [...selectedFiles, fileId];
|
||||
setSelectedFiles(newSelectedFiles);
|
||||
onSelectionChange(newSelectedFiles, selectedFolders);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=''>
|
||||
{authError && (
|
||||
<div className="text-red-500 text-sm mb-4 text-center">{authError}</div>
|
||||
)}
|
||||
|
||||
<ConnectorAuth
|
||||
provider={provider}
|
||||
onSuccess={(data) => {
|
||||
setUserEmail(data.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
if (data.session_token) {
|
||||
setSessionToken(provider, data.session_token);
|
||||
loadCloudFiles(data.session_token, null);
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
setAuthError(error);
|
||||
setIsConnected(false);
|
||||
}}
|
||||
isConnected={isConnected}
|
||||
userEmail={userEmail}
|
||||
onDisconnect={() => {
|
||||
const sessionToken = getSessionToken(provider);
|
||||
if (sessionToken) {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
fetch(`${apiHost}/api/connectors/disconnect`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ provider: provider, session_token: sessionToken })
|
||||
}).catch(err => console.error(`Error disconnecting from ${getProviderConfig(provider).displayName}:`, err));
|
||||
}
|
||||
|
||||
removeSessionToken(provider);
|
||||
setIsConnected(false);
|
||||
setFiles([]);
|
||||
setSelectedFiles([]);
|
||||
onSelectionChange([]);
|
||||
|
||||
if (onDisconnect) {
|
||||
onDisconnect();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{isConnected && (
|
||||
<div className="border border-[#D7D7D7] rounded-lg dark:border-[#6A6A6A] mt-3">
|
||||
<div className="border-[#EEE6FF78] dark:border-[#6A6A6A] rounded-t-lg">
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className="px-4 pt-4 bg-[#EEE6FF78] dark:bg-[#2A262E] rounded-t-lg">
|
||||
<div className="flex items-center gap-1 mb-2">
|
||||
{folderPath.map((path, index) => (
|
||||
<div key={path.id || 'root'} className="flex items-center gap-1">
|
||||
{index > 0 && <span className="text-gray-400">/</span>}
|
||||
<button
|
||||
onClick={() => navigateBack(index)}
|
||||
className="text-sm text-[#A076F6] hover:text-[#8A5FD4] hover:underline"
|
||||
disabled={index === folderPath.length - 1}
|
||||
>
|
||||
{path.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
Select Files from {getProviderConfig(provider).displayName}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 max-w-md">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search files and folders..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
colorVariant="silver"
|
||||
borderVariant="thin"
|
||||
labelBgClassName="bg-[#EEE6FF78] dark:bg-[#2A262E]"
|
||||
leftIcon={<img src={SearchIcon} alt="Search" width={16} height={16} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected Files Message */}
|
||||
<div className="pb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
{selectedFiles.length + selectedFolders.length} selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-72">
|
||||
<TableContainer
|
||||
ref={scrollContainerRef}
|
||||
height="288px"
|
||||
className="scrollbar-thin md:w-4xl lg:w-5xl"
|
||||
bordered={false}
|
||||
>
|
||||
{(
|
||||
<>
|
||||
<Table minWidth="1200px">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40px"></TableHeader>
|
||||
<TableHeader width="60%">Name</TableHeader>
|
||||
<TableHeader width="20%">Last Modified</TableHeader>
|
||||
<TableHeader width="20%">Size</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{files.map((file, index) => (
|
||||
<TableRow
|
||||
key={`${file.id}-${index}`}
|
||||
onClick={() => {
|
||||
if (isFolder(file)) {
|
||||
handleFolderClick(file.id, file.name);
|
||||
} else {
|
||||
handleFileSelect(file.id, false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TableCell width="40px" align="center">
|
||||
<div
|
||||
className="flex h-5 w-5 text-sm shrink-0 items-center justify-center border border-[#EEE6FF78] p-[0.5px] dark:border-[#6A6A6A] cursor-pointer mx-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleFileSelect(file.id, isFolder(file));
|
||||
}}
|
||||
>
|
||||
{(isFolder(file) ? selectedFolders : selectedFiles).includes(file.id) && (
|
||||
<img
|
||||
src={CheckIcon}
|
||||
alt="Selected"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
src={isFolder(file) ? FolderIcon : FileIcon}
|
||||
alt={isFolder(file) ? "Folder" : "File"}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate">{file.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='text-xs'>
|
||||
{formatDate(file.modifiedTime)}
|
||||
</TableCell>
|
||||
<TableCell className='text-xs'>
|
||||
{file.size ? formatBytes(file.size) : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center p-4 border-t border-[#EEE6FF78] dark:border-[#6A6A6A]">
|
||||
<div className="inline-flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
Loading more files...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,15 @@ import EyeView from '../assets/eye-view.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import { useOutsideAlerter } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from './Table';
|
||||
|
||||
interface FileNode {
|
||||
type?: string;
|
||||
@@ -533,32 +542,31 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const parentRow =
|
||||
currentPath.length > 0
|
||||
? [
|
||||
<tr
|
||||
key="parent-dir"
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
-
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4"></td>
|
||||
</tr>,
|
||||
]
|
||||
<TableRow
|
||||
key="parent-dir"
|
||||
onClick={navigateUp}
|
||||
>
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.parentFolderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
..
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
-
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right"></TableCell>
|
||||
</TableRow>,
|
||||
]
|
||||
: [];
|
||||
|
||||
// Render directories first, then files
|
||||
@@ -570,32 +578,31 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const dirStats = calculateDirectoryStats(node as DirectoryStructure);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => navigateToDirectory(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FolderIcon}
|
||||
alt={t('settings.sources.folderAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
{dirStats.totalTokens > 0
|
||||
? dirStats.totalTokens.toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
{dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -619,8 +626,8 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}),
|
||||
...files.map(([name, node]) => {
|
||||
@@ -628,30 +635,29 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
const menuRef = getMenuRef(itemId);
|
||||
|
||||
return (
|
||||
<tr
|
||||
<TableRow
|
||||
key={itemId}
|
||||
className="cursor-pointer border-b border-[#D1D9E0] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]"
|
||||
onClick={() => handleFileClick(name)}
|
||||
>
|
||||
<td className="px-2 py-2 lg:px-4">
|
||||
<TableCell width="40%" align="left">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<img
|
||||
src={FileIcon}
|
||||
alt={t('settings.sources.fileAlt')}
|
||||
className="mr-2 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<span className="truncate text-sm dark:text-[#E0E0E0]">
|
||||
<span className="truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm lg:px-4 dark:text-[#E0E0E0]">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm md:px-4 dark:text-[#E0E0E0]">
|
||||
</TableCell>
|
||||
<TableCell width="30%" align="left">
|
||||
{node.size_bytes ? formatBytes(node.size_bytes) : '-'}
|
||||
</td>
|
||||
<td className="w-10 px-2 py-2 text-sm lg:px-4">
|
||||
</TableCell>
|
||||
<TableCell width="20%" align="right">
|
||||
{node.token_count?.toLocaleString() || '-'}
|
||||
</TableCell>
|
||||
<TableCell width="10%" align="right">
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, itemId)}
|
||||
@@ -675,8 +681,8 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
offset={{ x: -4, y: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}),
|
||||
];
|
||||
@@ -828,31 +834,31 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
|
||||
<div className="mb-2">{renderPathNavigation()}</div>
|
||||
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<table className="w-full min-w-[600px] table-auto bg-transparent">
|
||||
<thead className="bg-gray-100 dark:bg-[#27282D]">
|
||||
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
|
||||
<th className="min-w-[200px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader width="40%" align="left">
|
||||
{t('settings.sources.fileName')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
{t('settings.sources.tokens')}
|
||||
</th>
|
||||
<th className="min-w-[80px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="30%" align="left">
|
||||
{t('settings.sources.size')}
|
||||
</th>
|
||||
<th className="w-[60px] px-2 py-3 text-left text-sm font-medium text-gray-700 lg:px-4 dark:text-[#59636E]">
|
||||
</TableHeader>
|
||||
<TableHeader width="20%" align="right">
|
||||
{t('settings.sources.tokens')}
|
||||
</TableHeader>
|
||||
<TableHeader width="10%" align="right">
|
||||
<span className="sr-only">
|
||||
{t('settings.sources.actions')}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="[&>tr:last-child]:border-b-0">
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renderFileTree(currentDirectory)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
342
frontend/src/components/GoogleDrivePicker.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import useDrivePicker from 'react-google-drive-picker';
|
||||
|
||||
import ConnectorAuth from './ConnectorAuth';
|
||||
import { getSessionToken, setSessionToken, removeSessionToken } from '../utils/providerUtils';
|
||||
|
||||
|
||||
interface PickerFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
iconUrl: string;
|
||||
description?: string;
|
||||
sizeBytes?: string;
|
||||
}
|
||||
|
||||
interface GoogleDrivePickerProps {
|
||||
token: string | null;
|
||||
onSelectionChange: (fileIds: string[], folderIds?: string[]) => void;
|
||||
}
|
||||
|
||||
const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
token,
|
||||
onSelectionChange,
|
||||
}) => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<PickerFile[]>([]);
|
||||
const [selectedFolders, setSelectedFolders] = useState<PickerFile[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [authError, setAuthError] = useState<string>('');
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
const [openPicker] = useDrivePicker();
|
||||
|
||||
useEffect(() => {
|
||||
const sessionToken = getSessionToken('google_drive');
|
||||
if (sessionToken) {
|
||||
setIsValidating(true);
|
||||
setIsConnected(true); // Optimistically set as connected for skeleton
|
||||
validateSession(sessionToken);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const validateSession = async (sessionToken: string) => {
|
||||
try {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
const validateResponse = await fetch(`${apiHost}/api/connectors/validate-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ provider: 'google_drive', session_token: sessionToken })
|
||||
});
|
||||
|
||||
if (!validateResponse.ok) {
|
||||
setIsConnected(false);
|
||||
setAuthError('Session expired. Please reconnect to Google Drive.');
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const validateData = await validateResponse.json();
|
||||
if (validateData.success) {
|
||||
setUserEmail(validateData.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
setAccessToken(validateData.access_token || null);
|
||||
setIsValidating(false);
|
||||
return true;
|
||||
} else {
|
||||
setIsConnected(false);
|
||||
setAuthError(validateData.error || 'Session expired. Please reconnect your account.');
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error validating session:', error);
|
||||
setAuthError('Failed to validate session. Please reconnect.');
|
||||
setIsConnected(false);
|
||||
setIsValidating(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenPicker = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const sessionToken = getSessionToken('google_drive');
|
||||
|
||||
if (!sessionToken) {
|
||||
setAuthError('No valid session found. Please reconnect to Google Drive.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
setAuthError('No access token available. Please reconnect to Google Drive.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const clientId: string = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
|
||||
// Derive appId from clientId (extract numeric part before first dash)
|
||||
const appId = clientId ? clientId.split('-')[0] : null;
|
||||
|
||||
if (!clientId || !appId) {
|
||||
console.error('Missing Google Drive configuration');
|
||||
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
openPicker({
|
||||
clientId: clientId,
|
||||
developerKey: "",
|
||||
appId: appId,
|
||||
setSelectFolderEnabled: false,
|
||||
viewId: "DOCS",
|
||||
showUploadView: false,
|
||||
showUploadFolders: false,
|
||||
supportDrives: false,
|
||||
multiselect: true,
|
||||
token: accessToken,
|
||||
viewMimeTypes: 'application/vnd.google-apps.document,application/vnd.google-apps.presentation,application/vnd.google-apps.spreadsheet,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/msword,application/vnd.ms-powerpoint,application/vnd.ms-excel,text/plain,text/csv,text/html,text/markdown,text/x-rst,application/json,application/epub+zip,application/rtf,image/jpeg,image/jpg,image/png',
|
||||
callbackFunction: (data:any) => {
|
||||
setIsLoading(false);
|
||||
if (data.action === 'picked') {
|
||||
const docs = data.docs;
|
||||
|
||||
const newFiles: PickerFile[] = [];
|
||||
const newFolders: PickerFile[] = [];
|
||||
|
||||
docs.forEach((doc: any) => {
|
||||
const item = {
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
mimeType: doc.mimeType,
|
||||
iconUrl: doc.iconUrl || '',
|
||||
description: doc.description,
|
||||
sizeBytes: doc.sizeBytes
|
||||
};
|
||||
|
||||
if (doc.mimeType === 'application/vnd.google-apps.folder') {
|
||||
newFolders.push(item);
|
||||
} else {
|
||||
newFiles.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedFiles(prevFiles => {
|
||||
const existingFileIds = new Set(prevFiles.map(file => file.id));
|
||||
const uniqueNewFiles = newFiles.filter(file => !existingFileIds.has(file.id));
|
||||
return [...prevFiles, ...uniqueNewFiles];
|
||||
});
|
||||
|
||||
setSelectedFolders(prevFolders => {
|
||||
const existingFolderIds = new Set(prevFolders.map(folder => folder.id));
|
||||
const uniqueNewFolders = newFolders.filter(folder => !existingFolderIds.has(folder.id));
|
||||
return [...prevFolders, ...uniqueNewFolders];
|
||||
});
|
||||
onSelectionChange(
|
||||
[...selectedFiles, ...newFiles].map(file => file.id),
|
||||
[...selectedFolders, ...newFolders].map(folder => folder.id)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error opening picker:', error);
|
||||
setAuthError('Failed to open file picker. Please try again.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
const sessionToken = getSessionToken('google_drive');
|
||||
if (sessionToken) {
|
||||
try {
|
||||
const apiHost = import.meta.env.VITE_API_HOST;
|
||||
await fetch(`${apiHost}/api/connectors/disconnect`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ provider: 'google_drive', session_token: sessionToken })
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error disconnecting from Google Drive:', err);
|
||||
}
|
||||
}
|
||||
|
||||
removeSessionToken('google_drive');
|
||||
setIsConnected(false);
|
||||
setSelectedFiles([]);
|
||||
setSelectedFolders([]);
|
||||
setAccessToken(null);
|
||||
setUserEmail('');
|
||||
setAuthError('');
|
||||
onSelectionChange([], []);
|
||||
};
|
||||
|
||||
const ConnectedStateSkeleton = () => (
|
||||
<div className="mb-4">
|
||||
<div className="w-full flex items-center justify-between rounded-[10px] bg-gray-200 dark:bg-gray-700 px-4 py-2 animate-pulse">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||
<div className="h-4 w-32 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
<div className="h-4 w-16 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const FilesSectionSkeleton = () => (
|
||||
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="h-5 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
<div className="h-8 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
</div>
|
||||
<div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isValidating ? (
|
||||
<>
|
||||
<ConnectedStateSkeleton />
|
||||
<FilesSectionSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ConnectorAuth
|
||||
provider="google_drive"
|
||||
label="Connect to Google Drive"
|
||||
onSuccess={(data) => {
|
||||
setUserEmail(data.user_email || 'Connected User');
|
||||
setIsConnected(true);
|
||||
setAuthError('');
|
||||
|
||||
if (data.session_token) {
|
||||
setSessionToken('google_drive', data.session_token);
|
||||
validateSession(data.session_token);
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
setAuthError(error);
|
||||
setIsConnected(false);
|
||||
}}
|
||||
isConnected={isConnected}
|
||||
userEmail={userEmail}
|
||||
onDisconnect={handleDisconnect}
|
||||
errorMessage={authError}
|
||||
/>
|
||||
|
||||
{isConnected && (
|
||||
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-medium">Selected Files</h3>
|
||||
<button
|
||||
onClick={() => handleOpenPicker()}
|
||||
className="bg-[#A076F6] hover:bg-[#8A5FD4] text-white text-sm py-1 px-3 rounded-md"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Select Files'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{selectedFiles.length === 0 && selectedFolders.length === 0 ? (
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm">No files or folders selected</p>
|
||||
) : (
|
||||
<div className="max-h-60 overflow-y-auto">
|
||||
{selectedFolders.length > 0 && (
|
||||
<div className="mb-2">
|
||||
<h4 className="text-xs font-medium text-gray-500 mb-1">Folders</h4>
|
||||
{selectedFolders.map((folder) => (
|
||||
<div key={folder.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<img src={folder.iconUrl} alt="Folder" className="w-5 h-5 mr-2" />
|
||||
<span className="text-sm truncate flex-1">{folder.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newSelectedFolders = selectedFolders.filter(f => f.id !== folder.id);
|
||||
setSelectedFolders(newSelectedFolders);
|
||||
onSelectionChange(
|
||||
selectedFiles.map(f => f.id),
|
||||
newSelectedFolders.map(f => f.id)
|
||||
);
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 text-sm ml-2"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFiles.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-gray-500 mb-1">Files</h4>
|
||||
{selectedFiles.map((file) => (
|
||||
<div key={file.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<img src={file.iconUrl} alt="File" className="w-5 h-5 mr-2" />
|
||||
<span className="text-sm truncate flex-1">{file.name}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newSelectedFiles = selectedFiles.filter(f => f.id !== file.id);
|
||||
setSelectedFiles(newSelectedFiles);
|
||||
onSelectionChange(
|
||||
newSelectedFiles.map(f => f.id),
|
||||
selectedFolders.map(f => f.id)
|
||||
);
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 text-sm ml-2"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleDrivePicker;
|
||||
@@ -16,6 +16,7 @@ const Input = ({
|
||||
textSize = 'medium',
|
||||
children,
|
||||
labelBgClassName = 'bg-white dark:bg-raisin-black',
|
||||
leftIcon,
|
||||
onChange,
|
||||
onPaste,
|
||||
onKeyDown,
|
||||
@@ -42,7 +43,7 @@ const Input = ({
|
||||
<div className={`relative ${className}`}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`peer text-jet dark:text-bright-gray h-[42px] w-full rounded-full bg-transparent px-3 py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||
className={`peer text-jet dark:text-bright-gray h-[42px] w-full rounded-full bg-transparent ${leftIcon ? 'pl-10' : 'px-3'} py-1 placeholder-transparent outline-hidden ${colorStyles[colorVariant]} ${borderStyles[borderVariant]} ${textSizeStyles[textSize]} [&:-webkit-autofill]:appearance-none [&:-webkit-autofill]:bg-transparent [&:-webkit-autofill_selected]:bg-transparent`}
|
||||
type={type}
|
||||
id={id}
|
||||
name={name}
|
||||
@@ -57,12 +58,19 @@ const Input = ({
|
||||
>
|
||||
{children}
|
||||
</input>
|
||||
{leftIcon && (
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 flex items-center justify-center">
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
{placeholder && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`absolute select-none ${
|
||||
hasValue ? '-top-2.5 left-3 text-xs' : ''
|
||||
} px-2 transition-all peer-placeholder-shown:top-2.5 peer-placeholder-shown:left-3 peer-placeholder-shown:${
|
||||
} px-2 transition-all peer-placeholder-shown:top-2.5 ${
|
||||
leftIcon ? 'peer-placeholder-shown:left-7' : 'peer-placeholder-shown:left-3'
|
||||
} peer-placeholder-shown:${
|
||||
textSizeStyles[textSize]
|
||||
} text-gray-4000 pointer-events-none cursor-none peer-focus:-top-2.5 peer-focus:left-3 peer-focus:text-xs dark:text-gray-400 ${labelBgClassName} max-w-[calc(100%-24px)] overflow-hidden text-ellipsis whitespace-nowrap`}
|
||||
>
|
||||
|
||||
172
frontend/src/components/Table.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TableProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
minWidth?: string;
|
||||
}
|
||||
|
||||
|
||||
interface TableContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
height?: string;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
interface TableHeadProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TableRowProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface TableCellProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
minWidth?: string;
|
||||
width?: string;
|
||||
align?: 'left' | 'right' | 'center';
|
||||
}
|
||||
|
||||
const TableContainer = React.forwardRef<HTMLDivElement, TableContainerProps>(({
|
||||
children,
|
||||
className = '',
|
||||
height = 'auto',
|
||||
bordered = true
|
||||
}, ref) => {
|
||||
return (
|
||||
<div className={`relative rounded-[6px] ${className}`}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`w-full overflow-x-auto rounded-[6px] bg-transparent ${bordered ? 'border border-[#D7D7D7] dark:border-[#6A6A6A]' : ''}`}
|
||||
style={{
|
||||
maxHeight: height === 'auto' ? undefined : height,
|
||||
overflowY: height === 'auto' ? 'hidden' : 'auto'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});;
|
||||
const Table: React.FC<TableProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth = 'min-w-[600px]'
|
||||
}) => {
|
||||
return (
|
||||
<table className={`w-full table-auto border-collapse bg-transparent ${minWidth} ${className}`}>
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
};
|
||||
const TableHead: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<thead className={`
|
||||
sticky top-0 z-10
|
||||
bg-gray-100 dark:bg-[#27282D]
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
};
|
||||
|
||||
const TableBody: React.FC<TableHeadProps> = ({ children, className = '' }) => {
|
||||
return (
|
||||
<tbody className={`[&>tr:last-child]:border-b-0 ${className}`}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
};
|
||||
|
||||
const TableRow: React.FC<TableRowProps> = ({ children, className = '', onClick }) => {
|
||||
const baseClasses = "border-b border-[#D7D7D7] hover:bg-[#ECEEEF] dark:border-[#6A6A6A] dark:hover:bg-[#27282D]";
|
||||
const cursorClass = onClick ? "cursor-pointer" : "";
|
||||
|
||||
return (
|
||||
<tr className={`${baseClasses} ${cursorClass} ${className}`} onClick={onClick}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const TableHeader: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth,
|
||||
width,
|
||||
align = 'left'
|
||||
}) => {
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return 'text-right';
|
||||
case 'center':
|
||||
return 'text-center';
|
||||
default:
|
||||
return 'text-left';
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses = `px-2 py-3 text-sm font-medium text-gray-700 lg:px-3 dark:text-[#59636E] border-b border-[#D7D7D7] dark:border-[#6A6A6A] relative box-border ${getAlignmentClass()}`;
|
||||
const widthClasses = minWidth ? minWidth : '';
|
||||
|
||||
return (
|
||||
<th
|
||||
className={`${baseClasses} ${widthClasses} ${className}`}
|
||||
style={width ? { width, minWidth: width, maxWidth: width } : {}}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
const TableCell: React.FC<TableCellProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
minWidth,
|
||||
width,
|
||||
align = 'left'
|
||||
}) => {
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return 'text-right';
|
||||
case 'center':
|
||||
return 'text-center';
|
||||
default:
|
||||
return 'text-left';
|
||||
}
|
||||
};
|
||||
|
||||
const baseClasses = `px-2 py-2 text-sm lg:px-3 dark:text-[#E0E0E0] box-border ${getAlignmentClass()}`;
|
||||
const widthClasses = minWidth ? minWidth : '';
|
||||
|
||||
return (
|
||||
<td
|
||||
className={`${baseClasses} ${widthClasses} ${className}`}
|
||||
style={width ? { width, minWidth: width, maxWidth: width } : {}}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -53,7 +53,7 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
|
||||
{label && (
|
||||
<span
|
||||
className={`text-eerie-black dark:text-white ${
|
||||
labelPosition === 'left' ? 'mr-1' : 'ml-1'
|
||||
labelPosition === 'left' ? 'mr-3' : 'ml-3'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type InputProps = {
|
||||
onKeyDown?: (
|
||||
e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>,
|
||||
) => void;
|
||||
leftIcon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type MermaidRendererProps = {
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function ConversationMessages({
|
||||
)}
|
||||
|
||||
<div className="w-full max-w-[1300px] px-2 md:w-9/12 lg:w-8/12 xl:w-8/12 2xl:w-6/12">
|
||||
{headerContent && headerContent}
|
||||
{headerContent}
|
||||
|
||||
{queries.length > 0 ? (
|
||||
queries.map((query, index) => (
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "Private",
|
||||
"sync": "Sync",
|
||||
"syncing": "Syncing...",
|
||||
"syncConfirmation": "Are you sure you want to sync \"{{sourceName}}\"? This will update the content with your cloud storage and may override any edits you made to individual chunks.",
|
||||
"syncFrequency": {
|
||||
"never": "Never",
|
||||
"daily": "Daily",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "Privado",
|
||||
"sync": "Sincronizar",
|
||||
"syncing": "Sincronizando...",
|
||||
"syncConfirmation": "¿Estás seguro de que deseas sincronizar \"{{sourceName}}\"? Esto actualizará el contenido con tu almacenamiento en la nube y puede anular cualquier edición que hayas realizado en fragmentos individuales.",
|
||||
"syncFrequency": {
|
||||
"never": "Nunca",
|
||||
"daily": "Diario",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "プライベート",
|
||||
"sync": "同期",
|
||||
"syncing": "同期中...",
|
||||
"syncConfirmation": "\"{{sourceName}}\"を同期してもよろしいですか?これにより、コンテンツがクラウドストレージで更新され、個々のチャンクに加えた編集が上書きされる可能性があります。",
|
||||
"syncFrequency": {
|
||||
"never": "なし",
|
||||
"daily": "毎日",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "Частный",
|
||||
"sync": "Синхронизация",
|
||||
"syncing": "Синхронизация...",
|
||||
"syncConfirmation": "Вы уверены, что хотите синхронизировать \"{{sourceName}}\"? Это обновит содержимое с вашим облачным хранилищем и может перезаписать любые изменения, внесенные вами в отдельные фрагменты.",
|
||||
"syncFrequency": {
|
||||
"never": "Никогда",
|
||||
"daily": "Ежедневно",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "私人",
|
||||
"sync": "同步",
|
||||
"syncing": "同步中...",
|
||||
"syncConfirmation": "您確定要同步 \"{{sourceName}}\" 嗎?這將使用您的雲端儲存更新內容,並可能覆蓋您對個別文本塊所做的任何編輯。",
|
||||
"syncFrequency": {
|
||||
"never": "從不",
|
||||
"daily": "每天",
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"private": "私有",
|
||||
"sync": "同步",
|
||||
"syncing": "同步中...",
|
||||
"syncConfirmation": "您确定要同步 \"{{sourceName}}\" 吗?这将使用您的云存储更新内容,并可能覆盖您对单个文本块所做的任何编辑。",
|
||||
"syncFrequency": {
|
||||
"never": "从不",
|
||||
"daily": "每天",
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function WrapperModal({
|
||||
<div className="fixed top-0 left-0 z-30 flex h-screen w-screen items-center justify-center">
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`relative w-11/12 rounded-2xl bg-white p-8 shadow-2xl sm:w-[512px] dark:bg-[#26272E] ${className}`}
|
||||
className={`relative rounded-2xl bg-white dark:bg-[#26272E] p-8 shadow-[0px_4px_40px_-3px_#0000001A] ${className}`}
|
||||
>
|
||||
{!isPerformingTask && (
|
||||
<button
|
||||
@@ -55,7 +55,7 @@ export default function WrapperModal({
|
||||
<img className="filter dark:invert" src={Exit} alt="Close" />
|
||||
</button>
|
||||
)}
|
||||
<div className={`${contentClassName}`}>{children}</div>
|
||||
<div className={`overflow-y-auto no-scrollbar text-[#18181B] dark:text-[#ECECF1] ${contentClassName}`}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -272,7 +272,7 @@ export default function Sources({
|
||||
return documentToView ? (
|
||||
<div className="mt-8 flex flex-col">
|
||||
{documentToView.isNested ? (
|
||||
documentToView.type === 'connector' ? (
|
||||
documentToView.type === 'connector:file' ? (
|
||||
<ConnectorTreeComponent
|
||||
docId={documentToView.id || ''}
|
||||
sourceName={documentToView.name}
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
export interface BaseIngestorConfig {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
}
|
||||
import CrawlerIcon from '../../assets/crawler.svg';
|
||||
import FileUploadIcon from '../../assets/file_upload.svg';
|
||||
import UrlIcon from '../../assets/url.svg';
|
||||
import GithubIcon from '../../assets/github.svg';
|
||||
import RedditIcon from '../../assets/reddit.svg';
|
||||
import DriveIcon from '../../assets/drive.svg';
|
||||
|
||||
export interface RedditIngestorConfig extends BaseIngestorConfig {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
user_agent: string;
|
||||
search_queries: string;
|
||||
number_posts: number;
|
||||
}
|
||||
|
||||
export interface GithubIngestorConfig extends BaseIngestorConfig {
|
||||
repo_url: string;
|
||||
}
|
||||
|
||||
export interface CrawlerIngestorConfig extends BaseIngestorConfig {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface UrlIngestorConfig extends BaseIngestorConfig {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface GoogleDriveIngestorConfig extends BaseIngestorConfig {
|
||||
folder_id?: string;
|
||||
file_ids?: string;
|
||||
recursive?: boolean;
|
||||
token_info?: any;
|
||||
}
|
||||
|
||||
export type IngestorType =
|
||||
| 'crawler'
|
||||
| 'github'
|
||||
| 'reddit'
|
||||
| 'url'
|
||||
| 'google_drive';
|
||||
export type IngestorType = 'crawler' | 'github' | 'reddit' | 'url' | 'google_drive' | 'local_file';
|
||||
|
||||
export interface IngestorConfig {
|
||||
type: IngestorType;
|
||||
type: IngestorType | null;
|
||||
name: string;
|
||||
config:
|
||||
| RedditIngestorConfig
|
||||
| GithubIngestorConfig
|
||||
| CrawlerIngestorConfig
|
||||
| UrlIngestorConfig
|
||||
| GoogleDriveIngestorConfig;
|
||||
config: Record<string, string | number | boolean | File[]>;
|
||||
}
|
||||
|
||||
export type IngestorFormData = {
|
||||
@@ -54,7 +20,7 @@ export type IngestorFormData = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type FieldType = 'string' | 'number' | 'enum' | 'boolean';
|
||||
export type FieldType = 'string' | 'number' | 'enum' | 'boolean' | 'local_file_picker' | 'remote_file_picker' | 'google_drive_picker';
|
||||
|
||||
export interface FormField {
|
||||
name: string;
|
||||
@@ -65,89 +31,82 @@ export interface FormField {
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
export const IngestorFormSchemas: Record<IngestorType, FormField[]> = {
|
||||
crawler: [
|
||||
{
|
||||
name: 'url',
|
||||
label: 'URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
url: [
|
||||
{
|
||||
name: 'url',
|
||||
label: 'URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
reddit: [
|
||||
{
|
||||
name: 'client_id',
|
||||
label: 'Client ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'client_secret',
|
||||
label: 'Client Secret',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'user_agent',
|
||||
label: 'User Agent',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'search_queries',
|
||||
label: 'Search Queries',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'number_posts',
|
||||
label: 'Number of Posts',
|
||||
type: 'number',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
github: [
|
||||
{
|
||||
name: 'repo_url',
|
||||
label: 'Repository URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
google_drive: [
|
||||
{
|
||||
name: 'recursive',
|
||||
label: 'Include subfolders',
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
export interface IngestorSchema {
|
||||
key: IngestorType;
|
||||
label: string;
|
||||
icon: string;
|
||||
heading: string;
|
||||
validate?: () => boolean;
|
||||
fields: FormField[];
|
||||
}
|
||||
|
||||
export const IngestorDefaultConfigs: Record<
|
||||
IngestorType,
|
||||
Omit<IngestorConfig, 'type'>
|
||||
> = {
|
||||
crawler: {
|
||||
name: '',
|
||||
config: {
|
||||
url: '',
|
||||
} as CrawlerIngestorConfig,
|
||||
export const IngestorFormSchemas: IngestorSchema[] = [
|
||||
{
|
||||
key: 'local_file',
|
||||
label: 'Upload File',
|
||||
icon: FileUploadIcon,
|
||||
heading: 'Upload new document',
|
||||
fields: [
|
||||
{ name: 'files', label: 'Select files', type: 'local_file_picker', required: true },
|
||||
]
|
||||
},
|
||||
url: {
|
||||
name: '',
|
||||
config: {
|
||||
url: '',
|
||||
} as UrlIngestorConfig,
|
||||
{
|
||||
key: 'crawler',
|
||||
label: 'Crawler',
|
||||
icon: CrawlerIcon,
|
||||
heading: 'Add content with Web Crawler',
|
||||
fields: [{ name: 'url', label: 'URL', type: 'string', required: true }]
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: 'Link',
|
||||
icon: UrlIcon,
|
||||
heading: 'Add content from URL',
|
||||
fields: [{ name: 'url', label: 'URL', type: 'string', required: true }]
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
label: 'GitHub',
|
||||
icon: GithubIcon,
|
||||
heading: 'Add content from GitHub',
|
||||
fields: [{ name: 'repo_url', label: 'Repository URL', type: 'string', required: true }]
|
||||
},
|
||||
{
|
||||
key: 'reddit',
|
||||
label: 'Reddit',
|
||||
icon: RedditIcon,
|
||||
heading: 'Add content from Reddit',
|
||||
fields: [
|
||||
{ name: 'client_id', label: 'Client ID', type: 'string', required: true },
|
||||
{ name: 'client_secret', label: 'Client Secret', type: 'string', required: true },
|
||||
{ name: 'user_agent', label: 'User Agent', type: 'string', required: true },
|
||||
{ name: 'search_queries', label: 'Search Queries', type: 'string', required: true },
|
||||
{ name: 'number_posts', label: 'Number of Posts', type: 'number', required: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'google_drive',
|
||||
label: 'Google Drive',
|
||||
icon: DriveIcon,
|
||||
heading: 'Upload from Google Drive',
|
||||
validate: () => {
|
||||
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||
return !!(googleClientId);
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'files',
|
||||
label: 'Select Files from Google Drive',
|
||||
type: 'google_drive_picker',
|
||||
required: true,
|
||||
}
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
export const IngestorDefaultConfigs: Record<IngestorType, Omit<IngestorConfig, 'type'>> = {
|
||||
crawler: { name: '', config: { url: '' } },
|
||||
url: { name: '', config: { url: '' } },
|
||||
reddit: {
|
||||
name: '',
|
||||
config: {
|
||||
@@ -155,21 +114,30 @@ export const IngestorDefaultConfigs: Record<
|
||||
client_secret: '',
|
||||
user_agent: '',
|
||||
search_queries: '',
|
||||
number_posts: 10,
|
||||
} as RedditIngestorConfig,
|
||||
},
|
||||
github: {
|
||||
name: '',
|
||||
config: {
|
||||
repo_url: '',
|
||||
} as GithubIngestorConfig,
|
||||
number_posts: 10
|
||||
}
|
||||
},
|
||||
github: { name: '', config: { repo_url: '' } },
|
||||
google_drive: {
|
||||
name: '',
|
||||
config: {
|
||||
folder_id: '',
|
||||
file_ids: '',
|
||||
recursive: true,
|
||||
} as GoogleDriveIngestorConfig,
|
||||
folder_ids: '',
|
||||
recursive: true
|
||||
}
|
||||
},
|
||||
local_file: { name: '', config: { files: [] } },
|
||||
};
|
||||
|
||||
export interface IngestorOption {
|
||||
label: string;
|
||||
value: IngestorType;
|
||||
icon: string;
|
||||
heading: string;
|
||||
}
|
||||
|
||||
export const getIngestorSchema = (key: IngestorType): IngestorSchema | undefined => {
|
||||
return IngestorFormSchemas.find(schema => schema.key === key);
|
||||
};
|
||||
|
||||
|
||||
|
||||