generated from template/astro-template
	temp
This commit is contained in:
		@@ -19,6 +19,10 @@ let proxy = {
 | 
			
		||||
    target: `${target}`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
  },
 | 
			
		||||
  '/root/resources/': {
 | 
			
		||||
    target: `${target}`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
  },
 | 
			
		||||
  '/user/login/': {
 | 
			
		||||
    target: `${target}`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
@@ -30,6 +34,7 @@ let proxy = {
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  // ...
 | 
			
		||||
  // site: 'https://kevisual.xiongxiao.me/root/astro',
 | 
			
		||||
  // base: isDev ? undefined : pkgs.basename,
 | 
			
		||||
  base: isDev ? undefined : pkgs.basename,
 | 
			
		||||
  integrations: [
 | 
			
		||||
    mdx(),
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@
 | 
			
		||||
    "react-draggable": "^4.4.6",
 | 
			
		||||
    "react-hook-form": "^7.56.4",
 | 
			
		||||
    "react-i18next": "^15.5.2",
 | 
			
		||||
    "react-resizable-panels": "^3.0.2",
 | 
			
		||||
    "react-sortablejs": "^6.1.4",
 | 
			
		||||
    "react-toastify": "^11.0.5",
 | 
			
		||||
    "sortablejs": "^1.15.6",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -116,6 +116,9 @@ importers:
 | 
			
		||||
      react-i18next:
 | 
			
		||||
        specifier: ^15.5.2
 | 
			
		||||
        version: 15.5.2(i18next@25.2.0(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
 | 
			
		||||
      react-resizable-panels:
 | 
			
		||||
        specifier: ^3.0.2
 | 
			
		||||
        version: 3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
 | 
			
		||||
      react-sortablejs:
 | 
			
		||||
        specifier: ^6.1.4
 | 
			
		||||
        version: 6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6)
 | 
			
		||||
@@ -635,67 +638,79 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-libvips-linux-arm@1.0.5':
 | 
			
		||||
    resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
 | 
			
		||||
    cpu: [arm]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-libvips-linux-s390x@1.0.4':
 | 
			
		||||
    resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
 | 
			
		||||
    cpu: [s390x]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-libvips-linux-x64@1.0.4':
 | 
			
		||||
    resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
 | 
			
		||||
    resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-libvips-linuxmusl-x64@1.0.4':
 | 
			
		||||
    resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linux-arm64@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linux-arm@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [arm]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linux-s390x@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [s390x]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linux-x64@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linuxmusl-arm64@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-linuxmusl-x64@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
 | 
			
		||||
    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@img/sharp-wasm32@0.33.5':
 | 
			
		||||
    resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
 | 
			
		||||
@@ -1190,56 +1205,67 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==}
 | 
			
		||||
    cpu: [arm]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-arm-musleabihf@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==}
 | 
			
		||||
    cpu: [arm]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-arm64-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-arm64-musl@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-loongarch64-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==}
 | 
			
		||||
    cpu: [loong64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-powerpc64le-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==}
 | 
			
		||||
    cpu: [ppc64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-riscv64-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==}
 | 
			
		||||
    cpu: [riscv64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-riscv64-musl@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==}
 | 
			
		||||
    cpu: [riscv64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-s390x-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==}
 | 
			
		||||
    cpu: [s390x]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-x64-gnu@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-linux-x64-musl@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@rollup/rollup-win32-arm64-msvc@4.40.1':
 | 
			
		||||
    resolution: {integrity: sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==}
 | 
			
		||||
@@ -1318,24 +1344,28 @@ packages:
 | 
			
		||||
    engines: {node: '>= 10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@tailwindcss/oxide-linux-arm64-musl@4.1.7':
 | 
			
		||||
    resolution: {integrity: sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==}
 | 
			
		||||
    engines: {node: '>= 10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@tailwindcss/oxide-linux-x64-gnu@4.1.7':
 | 
			
		||||
    resolution: {integrity: sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==}
 | 
			
		||||
    engines: {node: '>= 10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  '@tailwindcss/oxide-linux-x64-musl@4.1.7':
 | 
			
		||||
    resolution: {integrity: sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==}
 | 
			
		||||
    engines: {node: '>= 10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  '@tailwindcss/oxide-wasm32-wasi@4.1.7':
 | 
			
		||||
    resolution: {integrity: sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==}
 | 
			
		||||
@@ -2325,24 +2355,28 @@ packages:
 | 
			
		||||
    engines: {node: '>= 12.0.0'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  lightningcss-linux-arm64-musl@1.30.1:
 | 
			
		||||
    resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
 | 
			
		||||
    engines: {node: '>= 12.0.0'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  lightningcss-linux-x64-gnu@1.30.1:
 | 
			
		||||
    resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
 | 
			
		||||
    engines: {node: '>= 12.0.0'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [glibc]
 | 
			
		||||
 | 
			
		||||
  lightningcss-linux-x64-musl@1.30.1:
 | 
			
		||||
    resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
 | 
			
		||||
    engines: {node: '>= 12.0.0'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    libc: [musl]
 | 
			
		||||
 | 
			
		||||
  lightningcss-win32-arm64-msvc@1.30.1:
 | 
			
		||||
    resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
 | 
			
		||||
@@ -2901,6 +2935,12 @@ packages:
 | 
			
		||||
      '@types/react':
 | 
			
		||||
        optional: true
 | 
			
		||||
 | 
			
		||||
  react-resizable-panels@3.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-j4RNII75fnHkLnbsTb5G5YsDvJsSEZrJK2XSF2z0Tc2jIonYlIVir/Yh/5LvcUFCfs1HqrMAoiBFmIrRjC4XnA==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
 | 
			
		||||
      react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
 | 
			
		||||
 | 
			
		||||
  react-sortablejs@6.1.4:
 | 
			
		||||
    resolution: {integrity: sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
@@ -6499,6 +6539,11 @@ snapshots:
 | 
			
		||||
    optionalDependencies:
 | 
			
		||||
      '@types/react': 19.1.5
 | 
			
		||||
 | 
			
		||||
  react-resizable-panels@3.0.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
 | 
			
		||||
    dependencies:
 | 
			
		||||
      react: 19.1.0
 | 
			
		||||
      react-dom: 19.1.0(react@19.1.0)
 | 
			
		||||
 | 
			
		||||
  react-sortablejs@6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6):
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@types/sortablejs': 1.15.8
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								src/apps/ai-editor/content.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/apps/ai-editor/content.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import { useShallow } from 'zustand/shallow';
 | 
			
		||||
import { getCurrentContent, useMenuStore } from './store/menu';
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { checkText } from './modules/get-content-type';
 | 
			
		||||
// import { TextEditor } from '@kevisual/markdown-editor/tiptap/editor.ts';
 | 
			
		||||
 | 
			
		||||
export const Content: React.FC = () => {
 | 
			
		||||
  const { currentPath } = useMenuStore(
 | 
			
		||||
    useShallow((state) => ({
 | 
			
		||||
      currentPath: state.currentPath,
 | 
			
		||||
    })),
 | 
			
		||||
  );
 | 
			
		||||
  const [content, setContent] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchContent = async () => {
 | 
			
		||||
      if (currentPath) {
 | 
			
		||||
        const isText = checkText(currentPath);
 | 
			
		||||
        if (!isText.isText) {
 | 
			
		||||
          setContent('This file type is not supported for viewing.');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        setContent('Loading content...');
 | 
			
		||||
        const content = await getCurrentContent(currentPath);
 | 
			
		||||
        setContent(content);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    fetchContent();
 | 
			
		||||
  }, [currentPath]);
 | 
			
		||||
  const ref = useRef<HTMLDivElement>(null);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='h-full w-full overflow-y-auto scrollbar px-2'>
 | 
			
		||||
      <pre>{content}</pre>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,7 @@
 | 
			
		||||
import { ToastProvider } from '@/modules/toast/Provider';
 | 
			
		||||
import Sidebar from './sidebar';
 | 
			
		||||
import { Content } from './content';
 | 
			
		||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
 | 
			
		||||
 | 
			
		||||
export const App = () => {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -8,11 +11,33 @@ export const App = () => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const SidebarApp = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastProvider>
 | 
			
		||||
      <Sidebar />
 | 
			
		||||
    </ToastProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const AIEditor = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex h-full w-full flex-col items-center justify-center'>
 | 
			
		||||
      <div className='text-2xl font-bold'>AI Editor</div>
 | 
			
		||||
      <div className='mt-4 text-gray-500'>This is a placeholder for the AI Editor application.</div>
 | 
			
		||||
      <div style={{ height: 'calc(100vh - 50px)' }} className='flex w-full'>
 | 
			
		||||
        <div className='flex w-full'>
 | 
			
		||||
          <PanelGroup direction='horizontal'>
 | 
			
		||||
            <Panel minSize={20} defaultSize={30}>
 | 
			
		||||
              <Sidebar />
 | 
			
		||||
            </Panel>
 | 
			
		||||
            <PanelResizeHandle />
 | 
			
		||||
            <Panel minSize={30}>
 | 
			
		||||
              <div className='w-full h-full overflow-hidden'>
 | 
			
		||||
                <Content />
 | 
			
		||||
              </div>
 | 
			
		||||
            </Panel>
 | 
			
		||||
          </PanelGroup>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/apps/ai-editor/modules/get-content-type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/apps/ai-editor/modules/get-content-type.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
export const checkText = (filepath: string) => {
 | 
			
		||||
  const textExtensions = [
 | 
			
		||||
    '.txt',
 | 
			
		||||
    '.md',
 | 
			
		||||
    '.json',
 | 
			
		||||
    '.js',
 | 
			
		||||
    '.ts',
 | 
			
		||||
    '.html',
 | 
			
		||||
    '.css',
 | 
			
		||||
    '.xml',
 | 
			
		||||
    '.csv',
 | 
			
		||||
 | 
			
		||||
    '.tsx',
 | 
			
		||||
    '.jsx',
 | 
			
		||||
    '.mjs',
 | 
			
		||||
    '.cjs',
 | 
			
		||||
    '.vue',
 | 
			
		||||
 | 
			
		||||
    '.yaml',
 | 
			
		||||
    '.yml',
 | 
			
		||||
    '.log',
 | 
			
		||||
    '.conf',
 | 
			
		||||
    '.env',
 | 
			
		||||
    '.example', //
 | 
			
		||||
  ];
 | 
			
		||||
  const extension = filepath.split('.').pop()?.toLowerCase();
 | 
			
		||||
  return { isText: textExtensions.includes(`.${extension}`) };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										135
									
								
								src/apps/ai-editor/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/apps/ai-editor/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
import React, { useState, useMemo, useEffect } from 'react';
 | 
			
		||||
import { File, Folder, ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
 | 
			
		||||
import { useMenuStore, init } from './store/menu.ts';
 | 
			
		||||
import type { MenuItem } from './store/menu.ts';
 | 
			
		||||
 | 
			
		||||
// 辅助函数:获取目录的直接子项
 | 
			
		||||
const getDirectChildren = (path: string, menu: MenuItem[]): MenuItem[] => {
 | 
			
		||||
  const pathPrefix = path ? path + '/' : '';
 | 
			
		||||
 | 
			
		||||
  return menu.filter((item) => {
 | 
			
		||||
    if (!item.path.startsWith(pathPrefix)) return false;
 | 
			
		||||
    if (item.path === path) return false;
 | 
			
		||||
 | 
			
		||||
    const relativePath = item.path.slice(pathPrefix.length);
 | 
			
		||||
    return !relativePath.includes('/');
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface DirectoryItemProps {
 | 
			
		||||
  item: MenuItem;
 | 
			
		||||
  level: number;
 | 
			
		||||
  prefix?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DirectoryItem: React.FC<DirectoryItemProps> = ({ item, level, prefix }) => {
 | 
			
		||||
  const [expanded, setExpanded] = useState(false);
 | 
			
		||||
  const { menu } = useMenuStore();
 | 
			
		||||
  const children = useMemo(() => {
 | 
			
		||||
    const children = getDirectChildren(item.path, menu);
 | 
			
		||||
    return children.sort((a, b) => {
 | 
			
		||||
      if (a.type === 'directory' && b.type === 'file') return -1; // 目录排在前面
 | 
			
		||||
      if (a.type === 'file' && b.type === 'directory') return 1; // 文件排在后面
 | 
			
		||||
      return a.path.localeCompare(b.path); // 同类型按路径排序
 | 
			
		||||
    });
 | 
			
		||||
  }, [menu, item.path]);
 | 
			
		||||
  const directory = item.path.replace(prefix || '', '').replace(/\/$/, '') || '/'; // 去掉前缀和末尾斜杠
 | 
			
		||||
  const newPrefix = item.path + '/';
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div
 | 
			
		||||
        className='flex items-center cursor-pointer py-1 px-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded group'
 | 
			
		||||
        style={{ paddingLeft: `${level * 12}px` }}
 | 
			
		||||
        onClick={() => setExpanded(!expanded)}>
 | 
			
		||||
        {children.length > 0 ? (
 | 
			
		||||
          expanded ? (
 | 
			
		||||
            <ChevronDown className='w-4 h-4 mr-1 text-gray-500' />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <ChevronRight className='w-4 h-4 mr-1 text-gray-500' />
 | 
			
		||||
          )
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className='w-4 h-4 mr-1' />
 | 
			
		||||
        )}
 | 
			
		||||
        <Folder className='w-4 h-4 mr-2 text-yellow-500 shrink-0' />
 | 
			
		||||
        <span className='text-sm truncate'>{directory}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {expanded && children.length > 0 && (
 | 
			
		||||
        <div className='ml-2'>
 | 
			
		||||
          {children.map((child) =>
 | 
			
		||||
            child.type === 'directory' ? (
 | 
			
		||||
              <DirectoryItem key={child.path} item={child} prefix={newPrefix} level={level + 1} />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <FileItem key={child.path} item={child} prefix={newPrefix} level={level + 1} />
 | 
			
		||||
            ),
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface FileItemProps {
 | 
			
		||||
  item: MenuItem;
 | 
			
		||||
  level: number;
 | 
			
		||||
  prefix?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FileItem: React.FC<FileItemProps> = ({ item, level, prefix }) => {
 | 
			
		||||
  const { currentPath, setCurrentPath } = useMenuStore();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`flex items-center cursor-pointer py-1 px-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded ${
 | 
			
		||||
        currentPath === item.path ? 'bg-blue-100 dark:bg-blue-900' : ''
 | 
			
		||||
      }`}
 | 
			
		||||
      style={{ paddingLeft: `${level * 12}px` }}
 | 
			
		||||
      onClick={() => setCurrentPath(item.path)}>
 | 
			
		||||
      <div className='w-4 h-4 mr-1' /> {/* 占位,保持对齐 */}
 | 
			
		||||
      <File className='w-4 h-4 mr-2 text-blue-500 shrink-0' />
 | 
			
		||||
      <span className='text-sm truncate'>{item.path.replace(prefix || '', '')}</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Sidebar: React.FC = () => {
 | 
			
		||||
  const { menu, isLoading } = useMenuStore();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // 初始化菜单数据
 | 
			
		||||
    init();
 | 
			
		||||
  }, []);
 | 
			
		||||
  // 获取顶层项(根目录下的文件和文件夹)
 | 
			
		||||
  const rootItems = useMemo(() => {
 | 
			
		||||
    // return menu.filter((item) => !item.path.includes('/'));
 | 
			
		||||
    const _menu = menu.filter((item) => {
 | 
			
		||||
      return !item.path.includes('/');
 | 
			
		||||
    });
 | 
			
		||||
    return _menu.sort((a, b) => {
 | 
			
		||||
      if (a.type === 'directory' && b.type === 'file') return -1; // 目录排在前面
 | 
			
		||||
      if (a.type === 'file' && b.type === 'directory') return 1; // 文件排在后面
 | 
			
		||||
      return a.path.localeCompare(b.path); // 同类型按路径排序
 | 
			
		||||
    });
 | 
			
		||||
  }, [menu]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='border-r w-full border-gray-200 dark:border-gray-700 min-h-screen p-2 bg-white dark:bg-gray-900 h-full'>
 | 
			
		||||
      <div className='flex items-center justify-between mb-4 px-2'>
 | 
			
		||||
        {/* <h2 className='text-lg font-semibold'>文件</h2> */}
 | 
			
		||||
        {isLoading && <Loader2 className='w-4 h-4 animate-spin text-gray-500' />}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='overflow-y-auto scrollbar' style={{ maxHeight: 'calc(100% - 58px)' }}>
 | 
			
		||||
        {menu.length === 0 && !isLoading ? (
 | 
			
		||||
          <div className='text-sm text-gray-500 px-2'>暂无文件</div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className='space-y-1'>
 | 
			
		||||
            {rootItems.map((item) =>
 | 
			
		||||
              item.type === 'directory' ? <DirectoryItem key={item.path} item={item} level={1} /> : <FileItem key={item.path} item={item} level={1} />,
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Sidebar;
 | 
			
		||||
							
								
								
									
										104
									
								
								src/apps/ai-editor/store/menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/apps/ai-editor/store/menu.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
import { create } from 'zustand';
 | 
			
		||||
import { query } from '@/modules/query';
 | 
			
		||||
import { QueryLoginBrowser } from '@/query/query-login/query-login-browser';
 | 
			
		||||
import { QueryResources } from '@/query/query-resources/index';
 | 
			
		||||
 | 
			
		||||
export const queryLogin = new QueryLoginBrowser({ query });
 | 
			
		||||
export const queryResources = new QueryResources({
 | 
			
		||||
  prefix: '/root/resources/',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type MenuItem = {
 | 
			
		||||
  type: 'file' | 'directory';
 | 
			
		||||
  name?: string; // type === 'file'
 | 
			
		||||
  prefix?: string; // type === 'directory'
 | 
			
		||||
  etag?: string;
 | 
			
		||||
  lastModified?: string; // ISO date string
 | 
			
		||||
  size: number;
 | 
			
		||||
  url: string;
 | 
			
		||||
  path: string; // a/b/file.md
 | 
			
		||||
  pathname: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Menu = MenuItem[];
 | 
			
		||||
 | 
			
		||||
interface MenuState {
 | 
			
		||||
  menu: Menu;
 | 
			
		||||
  currentPath: string | null;
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
  setMenu: (menu: Menu) => void;
 | 
			
		||||
  setCurrentPath: (path: string | null) => void;
 | 
			
		||||
  setLoading: (status: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useMenuStore = create<MenuState>((set) => ({
 | 
			
		||||
  menu: [],
 | 
			
		||||
  currentPath: null,
 | 
			
		||||
  isLoading: false,
 | 
			
		||||
  setMenu: (menu) => set({ menu }),
 | 
			
		||||
  setCurrentPath: (currentPath) => set({ currentPath }),
 | 
			
		||||
  setLoading: (isLoading) => set({ isLoading }),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
class Status {
 | 
			
		||||
  isInitialized = false;
 | 
			
		||||
  username = '';
 | 
			
		||||
}
 | 
			
		||||
const status = new Status();
 | 
			
		||||
 | 
			
		||||
export const init = async (prefix: string = '') => {
 | 
			
		||||
  const { setMenu, setCurrentPath, setLoading } = useMenuStore.getState();
 | 
			
		||||
  let me = await queryLogin.checkLocalUser();
 | 
			
		||||
  const isInitialized = status.isInitialized;
 | 
			
		||||
  if (!isInitialized && me) {
 | 
			
		||||
    status.isInitialized = true;
 | 
			
		||||
    status.username = me.username!;
 | 
			
		||||
    queryResources.setUsername(status.username);
 | 
			
		||||
  }
 | 
			
		||||
  let recursive = true;
 | 
			
		||||
  const data = {};
 | 
			
		||||
  if (recursive) {
 | 
			
		||||
    data['recursive'] = recursive;
 | 
			
		||||
  }
 | 
			
		||||
  const res = await queryResources.getList(prefix, data);
 | 
			
		||||
  if (res.code === 200) {
 | 
			
		||||
    const menu = res.data!.map((item: any) => {
 | 
			
		||||
      if (item.prefix) {
 | 
			
		||||
        item.type = 'directory';
 | 
			
		||||
      } else {
 | 
			
		||||
        item.type = 'file';
 | 
			
		||||
      }
 | 
			
		||||
      return item;
 | 
			
		||||
    });
 | 
			
		||||
    console.log('init menu', menu);
 | 
			
		||||
    if (recursive) {
 | 
			
		||||
      const obj: Record<string, any> = {};
 | 
			
		||||
      menu.forEach((item) => {
 | 
			
		||||
        const parts = item.path.split('/');
 | 
			
		||||
        const dirParts = parts.slice(0, -1);
 | 
			
		||||
        for (let i = 0; i < dirParts.length; i++) {
 | 
			
		||||
          const dir = dirParts.slice(0, i + 1).join('/');
 | 
			
		||||
          if (!dir) continue; // skip root
 | 
			
		||||
          obj[dir] = obj[dir] || { type: 'directory', path: dir };
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      Object.keys(obj).forEach((key) => {
 | 
			
		||||
        const item = obj[key];
 | 
			
		||||
        menu.push(item);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    setMenu(menu);
 | 
			
		||||
    setCurrentPath('');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getCurrentContent = async (currentPath: string): Promise<string | null> => {
 | 
			
		||||
  if (!currentPath) return null;
 | 
			
		||||
  const res = await queryResources.fetchFile(currentPath);
 | 
			
		||||
  if (res.success) {
 | 
			
		||||
    return res.data;
 | 
			
		||||
  } else {
 | 
			
		||||
    console.error('Error fetching content:', res.message);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										10
									
								
								src/pages/ai-editor/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/pages/ai-editor/index.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
---
 | 
			
		||||
import '@/styles/global.css';
 | 
			
		||||
import '@/styles/theme.css';
 | 
			
		||||
import Blank from '@/components/html/blank.astro';
 | 
			
		||||
import { App } from '@/apps/ai-editor';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<Blank>
 | 
			
		||||
  <App client:only />
 | 
			
		||||
</Blank>
 | 
			
		||||
							
								
								
									
										10
									
								
								src/pages/ai-editor/sidebar.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/pages/ai-editor/sidebar.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
---
 | 
			
		||||
import '@/styles/global.css';
 | 
			
		||||
import '@/styles/theme.css';
 | 
			
		||||
import Blank from '@/components/html/blank.astro';
 | 
			
		||||
import { SidebarApp } from '@/apps/ai-editor';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<Blank>
 | 
			
		||||
  <SidebarApp client:only />
 | 
			
		||||
</Blank>
 | 
			
		||||
							
								
								
									
										71
									
								
								src/query/query-resources/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/query/query-resources/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
import { adapter, DataOpts, Result } from '@kevisual/query';
 | 
			
		||||
 | 
			
		||||
type QueryResourcesOptions = {
 | 
			
		||||
  prefix?: string;
 | 
			
		||||
  storage?: Storage;
 | 
			
		||||
  username?: string;
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
};
 | 
			
		||||
export class QueryResources {
 | 
			
		||||
  prefix: string; // root/resources
 | 
			
		||||
  storage: Storage;
 | 
			
		||||
  constructor(opts: QueryResourcesOptions) {
 | 
			
		||||
    if (opts.username) {
 | 
			
		||||
      this.prefix = `/${opts.username}/resources/`;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.prefix = opts.prefix || '';
 | 
			
		||||
    }
 | 
			
		||||
    this.storage = opts.storage || localStorage;
 | 
			
		||||
  }
 | 
			
		||||
  setUsername(username: string) {
 | 
			
		||||
    this.prefix = `/${username}/resources/`;
 | 
			
		||||
  }
 | 
			
		||||
  header(headers?: Record<string, string>, json = true): Record<string, string> {
 | 
			
		||||
    const token = this.storage.getItem('token');
 | 
			
		||||
    const _headers: Record<string, string> = {
 | 
			
		||||
      'Content-Type': 'application/json',
 | 
			
		||||
      ...headers,
 | 
			
		||||
    };
 | 
			
		||||
    if (!json) {
 | 
			
		||||
      delete _headers['Content-Type'];
 | 
			
		||||
    }
 | 
			
		||||
    if (!token) {
 | 
			
		||||
      return _headers;
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      ..._headers,
 | 
			
		||||
      Authorization: `Bearer ${token}`,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  async get(data: any, opts: DataOpts): Promise<any> {
 | 
			
		||||
    return adapter({
 | 
			
		||||
      url: opts.url!,
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      body: data,
 | 
			
		||||
      ...opts,
 | 
			
		||||
      headers: this.header(opts?.headers),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
 | 
			
		||||
    return this.get(data, {
 | 
			
		||||
      url: `${this.prefix}${prefix}`,
 | 
			
		||||
      body: data,
 | 
			
		||||
      ...opts,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
 | 
			
		||||
    return fetch(`${this.prefix}${filepath}`, {
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      headers: this.header(opts?.headers, false),
 | 
			
		||||
    }).then(async (res) => {
 | 
			
		||||
      if (!res.ok) {
 | 
			
		||||
        return {
 | 
			
		||||
          code: 500,
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: `Failed to fetch file: ${res.status} ${res.statusText}`,
 | 
			
		||||
        } as Result<any>;
 | 
			
		||||
      }
 | 
			
		||||
      return { code: 200, data: await res.text(), success: true } as Result<any>;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user