feat: [frontend] support vision (#1518)

Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
zxhlyh
2023-11-13 22:32:39 +08:00
committed by GitHub
parent 41d0a8b295
commit 6b15827246
74 changed files with 3159 additions and 339 deletions

View File

@@ -6,6 +6,7 @@ type IProps = {
value: string
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
className?: string
wrapperClassName?: string
minHeight?: number
maxHeight?: number
autoFocus?: boolean
@@ -16,7 +17,7 @@ type IProps = {
const AutoHeightTextarea = forwardRef(
(
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
{ value, onChange, placeholder, className, wrapperClassName, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
outerRef: any,
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -54,7 +55,7 @@ const AutoHeightTextarea = forwardRef(
}, [controlFocus])
return (
<div className='relative'>
<div className={`relative ${wrapperClassName}`}>
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{
minHeight,
maxHeight,
@@ -80,4 +81,6 @@ const AutoHeightTextarea = forwardRef(
},
)
AutoHeightTextarea.displayName = 'AutoHeightTextarea'
export default AutoHeightTextarea

View File

@@ -0,0 +1,8 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="link-03">
<g id="Solid">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.01569 1.83378C9.7701 1.10515 10.7805 0.701975 11.8293 0.711089C12.8781 0.720202 13.8813 1.14088 14.623 1.88251C15.3646 2.62414 15.7853 3.62739 15.7944 4.67618C15.8035 5.72497 15.4003 6.73538 14.6717 7.48979L14.6636 7.49805L12.6637 9.49796C12.2581 9.90362 11.7701 10.2173 11.2327 10.4178C10.6953 10.6183 10.1211 10.7008 9.54897 10.6598C8.97686 10.6189 8.42025 10.4553 7.91689 10.1803C7.41354 9.90531 6.97522 9.52527 6.63165 9.06596C6.41112 8.77113 6.47134 8.35334 6.76618 8.1328C7.06101 7.91226 7.4788 7.97249 7.69934 8.26732C7.92838 8.57353 8.2206 8.82689 8.55617 9.01023C8.89174 9.19356 9.26281 9.30259 9.64422 9.3299C10.0256 9.35722 10.4085 9.30219 10.7667 9.16854C11.125 9.0349 11.4503 8.82576 11.7207 8.55532L13.7164 6.55956C14.1998 6.05705 14.4672 5.38513 14.4611 4.68777C14.455 3.98857 14.1746 3.31974 13.6802 2.82532C13.1857 2.3309 12.5169 2.05045 11.8177 2.04437C11.12 2.03831 10.4478 2.30591 9.94526 2.78967L8.80219 3.92609C8.54108 4.18568 8.11898 4.18445 7.85939 3.92334C7.5998 3.66223 7.60103 3.24012 7.86214 2.98053L9.0088 1.84053L9.01569 1.83378Z" fill="#667085"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.76493 5.58217C6.30234 5.3817 6.87657 5.29915 7.44869 5.34012C8.0208 5.3811 8.57741 5.54463 9.08077 5.81964C9.58412 6.09465 10.0224 6.47469 10.366 6.93399C10.5865 7.22882 10.5263 7.64662 10.2315 7.86715C9.93665 8.08769 9.51886 8.02746 9.29832 7.73263C9.06928 7.42643 8.77706 7.17307 8.44149 6.98973C8.10592 6.80639 7.73485 6.69737 7.35344 6.67005C6.97203 6.64274 6.58921 6.69777 6.23094 6.83141C5.87266 6.96506 5.54733 7.17419 5.27699 7.44463L3.28123 9.44039C2.79787 9.94291 2.5305 10.6148 2.53656 11.3122C2.54263 12.0114 2.82309 12.6802 3.31751 13.1746C3.81193 13.6691 4.48076 13.9495 5.17995 13.9556C5.87732 13.9616 6.54923 13.6943 7.05174 13.2109L8.18743 12.0752C8.44777 11.8149 8.86988 11.8149 9.13023 12.0752C9.39058 12.3356 9.39058 12.7577 9.13023 13.018L7.99023 14.158L7.98197 14.1662C7.22756 14.8948 6.21715 15.298 5.16837 15.2889C4.11958 15.2798 3.11633 14.8591 2.3747 14.1174C1.63307 13.3758 1.21239 12.3726 1.20328 11.3238C1.19416 10.275 1.59734 9.26458 2.32597 8.51017L2.33409 8.50191L4.33401 6.50199C4.33398 6.50202 4.33404 6.50196 4.33401 6.50199C4.7395 6.09638 5.22756 5.78262 5.76493 5.58217Z" fill="#667085"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Left Icon" clip-path="url(#clip0_12728_40636)">
<path id="Icon" d="M10.6654 8.00016L7.9987 5.3335M7.9987 5.3335L5.33203 8.00016M7.9987 5.3335V10.6668M14.6654 8.00016C14.6654 11.6821 11.6806 14.6668 7.9987 14.6668C4.3168 14.6668 1.33203 11.6821 1.33203 8.00016C1.33203 4.31826 4.3168 1.3335 7.9987 1.3335C11.6806 1.3335 14.6654 4.31826 14.6654 8.00016Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_12728_40636">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="image-plus">
<path id="Icon" d="M8.33333 2.00016H5.2C4.0799 2.00016 3.51984 2.00016 3.09202 2.21815C2.71569 2.4099 2.40973 2.71586 2.21799 3.09218C2 3.52001 2 4.08006 2 5.20016V10.8002C2 11.9203 2 12.4803 2.21799 12.9081C2.40973 13.2845 2.71569 13.5904 3.09202 13.7822C3.51984 14.0002 4.07989 14.0002 5.2 14.0002H11.3333C11.9533 14.0002 12.2633 14.0002 12.5176 13.932C13.2078 13.7471 13.7469 13.208 13.9319 12.5178C14 12.2635 14 11.9535 14 11.3335M12.6667 5.3335V1.3335M10.6667 3.3335H14.6667M7 5.66683C7 6.40321 6.40305 7.00016 5.66667 7.00016C4.93029 7.00016 4.33333 6.40321 4.33333 5.66683C4.33333 4.93045 4.93029 4.3335 5.66667 4.3335C6.40305 4.3335 7 4.93045 7 5.66683ZM9.99336 7.94559L4.3541 13.0722C4.03691 13.3605 3.87831 13.5047 3.86429 13.6296C3.85213 13.7379 3.89364 13.8453 3.97546 13.9172C4.06985 14.0002 4.28419 14.0002 4.71286 14.0002H10.9707C11.9301 14.0002 12.4098 14.0002 12.7866 13.839C13.2596 13.6366 13.6365 13.2598 13.8388 12.7868C14 12.41 14 11.9303 14 10.9708C14 10.648 14 10.4866 13.9647 10.3363C13.9204 10.1474 13.8353 9.9704 13.7155 9.81776C13.6202 9.6963 13.4941 9.59546 13.242 9.3938L11.3772 7.90194C11.1249 7.7001 10.9988 7.59919 10.8599 7.56357C10.7374 7.53218 10.6086 7.53624 10.4884 7.57529C10.352 7.61959 10.2324 7.72826 9.99336 7.94559Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C9.13833 4 6.80535 5.26472 5.07675 6.70743C3.3505 8.14818 2.16697 9.81429 1.57422 10.7528L1.55014 10.7908C1.43252 10.976 1.27981 11.2164 1.2026 11.5532C1.14027 11.8251 1.14027 12.1749 1.2026 12.4468C1.2798 12.7836 1.43252 13.024 1.55014 13.2092L1.57423 13.2472C2.16697 14.1857 3.3505 15.8518 5.07675 17.2926C6.80535 18.7353 9.13833 20 12 20C14.8617 20 17.1947 18.7353 18.9233 17.2926C20.6495 15.8518 21.833 14.1857 22.4258 13.2472L22.4499 13.2092C22.5675 13.024 22.7202 12.7837 22.7974 12.4468C22.8597 12.1749 22.8597 11.8251 22.7974 11.5532C22.7202 11.2163 22.5675 10.976 22.4499 10.7908L22.4258 10.7528C21.833 9.81429 20.6495 8.14818 18.9233 6.70743C17.1947 5.26472 14.8617 4 12 4ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,57 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "17",
"height": "16",
"viewBox": "0 0 17 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "link-03"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Solid"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M9.01569 1.83378C9.7701 1.10515 10.7805 0.701975 11.8293 0.711089C12.8781 0.720202 13.8813 1.14088 14.623 1.88251C15.3646 2.62414 15.7853 3.62739 15.7944 4.67618C15.8035 5.72497 15.4003 6.73538 14.6717 7.48979L14.6636 7.49805L12.6637 9.49796C12.2581 9.90362 11.7701 10.2173 11.2327 10.4178C10.6953 10.6183 10.1211 10.7008 9.54897 10.6598C8.97686 10.6189 8.42025 10.4553 7.91689 10.1803C7.41354 9.90531 6.97522 9.52527 6.63165 9.06596C6.41112 8.77113 6.47134 8.35334 6.76618 8.1328C7.06101 7.91226 7.4788 7.97249 7.69934 8.26732C7.92838 8.57353 8.2206 8.82689 8.55617 9.01023C8.89174 9.19356 9.26281 9.30259 9.64422 9.3299C10.0256 9.35722 10.4085 9.30219 10.7667 9.16854C11.125 9.0349 11.4503 8.82576 11.7207 8.55532L13.7164 6.55956C14.1998 6.05705 14.4672 5.38513 14.4611 4.68777C14.455 3.98857 14.1746 3.31974 13.6802 2.82532C13.1857 2.3309 12.5169 2.05045 11.8177 2.04437C11.12 2.03831 10.4478 2.30591 9.94526 2.78967L8.80219 3.92609C8.54108 4.18568 8.11898 4.18445 7.85939 3.92334C7.5998 3.66223 7.60103 3.24012 7.86214 2.98053L9.0088 1.84053L9.01569 1.83378Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M5.76493 5.58217C6.30234 5.3817 6.87657 5.29915 7.44869 5.34012C8.0208 5.3811 8.57741 5.54463 9.08077 5.81964C9.58412 6.09465 10.0224 6.47469 10.366 6.93399C10.5865 7.22882 10.5263 7.64662 10.2315 7.86715C9.93665 8.08769 9.51886 8.02746 9.29832 7.73263C9.06928 7.42643 8.77706 7.17307 8.44149 6.98973C8.10592 6.80639 7.73485 6.69737 7.35344 6.67005C6.97203 6.64274 6.58921 6.69777 6.23094 6.83141C5.87266 6.96506 5.54733 7.17419 5.27699 7.44463L3.28123 9.44039C2.79787 9.94291 2.5305 10.6148 2.53656 11.3122C2.54263 12.0114 2.82309 12.6802 3.31751 13.1746C3.81193 13.6691 4.48076 13.9495 5.17995 13.9556C5.87732 13.9616 6.54923 13.6943 7.05174 13.2109L8.18743 12.0752C8.44777 11.8149 8.86988 11.8149 9.13023 12.0752C9.39058 12.3356 9.39058 12.7577 9.13023 13.018L7.99023 14.158L7.98197 14.1662C7.22756 14.8948 6.21715 15.298 5.16837 15.2889C4.11958 15.2798 3.11633 14.8591 2.3747 14.1174C1.63307 13.3758 1.21239 12.3726 1.20328 11.3238C1.19416 10.275 1.59734 9.26458 2.32597 8.51017L2.33409 8.50191L4.33401 6.50199C4.33398 6.50202 4.33404 6.50196 4.33401 6.50199C4.7395 6.09638 5.22756 5.78262 5.76493 5.58217Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Link03"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Link03.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Link03'
export default Icon

View File

@@ -0,0 +1,66 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Left Icon",
"clip-path": "url(#clip0_12728_40636)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M10.6654 8.00016L7.9987 5.3335M7.9987 5.3335L5.33203 8.00016M7.9987 5.3335V10.6668M14.6654 8.00016C14.6654 11.6821 11.6806 14.6668 7.9987 14.6668C4.3168 14.6668 1.33203 11.6821 1.33203 8.00016C1.33203 4.31826 4.3168 1.3335 7.9987 1.3335C11.6806 1.3335 14.6654 4.31826 14.6654 8.00016Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_12728_40636"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "16",
"height": "16",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "Upload03"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Upload03.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Upload03'
export default Icon

View File

@@ -7,6 +7,7 @@ export { default as Edit03 } from './Edit03'
export { default as Hash02 } from './Hash02'
export { default as HelpCircle } from './HelpCircle'
export { default as InfoCircle } from './InfoCircle'
export { default as Link03 } from './Link03'
export { default as LinkExternal01 } from './LinkExternal01'
export { default as LinkExternal02 } from './LinkExternal02'
export { default as Loading02 } from './Loading02'
@@ -18,5 +19,6 @@ export { default as Settings01 } from './Settings01'
export { default as Settings04 } from './Settings04'
export { default as Target04 } from './Target04'
export { default as Trash03 } from './Trash03'
export { default as Upload03 } from './Upload03'
export { default as XClose } from './XClose'
export { default as X } from './X'

View File

@@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "image-plus"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M8.33333 2.00016H5.2C4.0799 2.00016 3.51984 2.00016 3.09202 2.21815C2.71569 2.4099 2.40973 2.71586 2.21799 3.09218C2 3.52001 2 4.08006 2 5.20016V10.8002C2 11.9203 2 12.4803 2.21799 12.9081C2.40973 13.2845 2.71569 13.5904 3.09202 13.7822C3.51984 14.0002 4.07989 14.0002 5.2 14.0002H11.3333C11.9533 14.0002 12.2633 14.0002 12.5176 13.932C13.2078 13.7471 13.7469 13.208 13.9319 12.5178C14 12.2635 14 11.9535 14 11.3335M12.6667 5.3335V1.3335M10.6667 3.3335H14.6667M7 5.66683C7 6.40321 6.40305 7.00016 5.66667 7.00016C4.93029 7.00016 4.33333 6.40321 4.33333 5.66683C4.33333 4.93045 4.93029 4.3335 5.66667 4.3335C6.40305 4.3335 7 4.93045 7 5.66683ZM9.99336 7.94559L4.3541 13.0722C4.03691 13.3605 3.87831 13.5047 3.86429 13.6296C3.85213 13.7379 3.89364 13.8453 3.97546 13.9172C4.06985 14.0002 4.28419 14.0002 4.71286 14.0002H10.9707C11.9301 14.0002 12.4098 14.0002 12.7866 13.839C13.2596 13.6366 13.6365 13.2598 13.8388 12.7868C14 12.41 14 11.9303 14 10.9708C14 10.648 14 10.4866 13.9647 10.3363C13.9204 10.1474 13.8353 9.9704 13.7155 9.81776C13.6202 9.6963 13.4941 9.59546 13.242 9.3938L11.3772 7.90194C11.1249 7.7001 10.9988 7.59919 10.8599 7.56357C10.7374 7.53218 10.6086 7.53624 10.4884 7.57529C10.352 7.61959 10.2324 7.72826 9.99336 7.94559Z",
"stroke": "currentColor",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "ImagePlus"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ImagePlus.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ImagePlus'
export default Icon

View File

@@ -0,0 +1 @@
export { default as ImagePlus } from './ImagePlus'

View File

@@ -0,0 +1,37 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M12 4C9.13833 4 6.80535 5.26472 5.07675 6.70743C3.3505 8.14818 2.16697 9.81429 1.57422 10.7528L1.55014 10.7908C1.43252 10.976 1.27981 11.2164 1.2026 11.5532C1.14027 11.8251 1.14027 12.1749 1.2026 12.4468C1.2798 12.7836 1.43252 13.024 1.55014 13.2092L1.57423 13.2472C2.16697 14.1857 3.3505 15.8518 5.07675 17.2926C6.80535 18.7353 9.13833 20 12 20C14.8617 20 17.1947 18.7353 18.9233 17.2926C20.6495 15.8518 21.833 14.1857 22.4258 13.2472L22.4499 13.2092C22.5675 13.024 22.7202 12.7837 22.7974 12.4468C22.8597 12.1749 22.8597 11.8251 22.7974 11.5532C22.7202 11.2163 22.5675 10.976 22.4499 10.7908L22.4258 10.7528C21.833 9.81429 20.6495 8.14818 18.9233 6.70743C17.1947 5.26472 14.8617 4 12 4ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Eye"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Eye.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Eye'
export default Icon

View File

@@ -1,6 +1,7 @@
export { default as CheckCircle } from './CheckCircle'
export { default as CheckDone01 } from './CheckDone01'
export { default as Download02 } from './Download02'
export { default as Eye } from './Eye'
export { default as MessageClockCircle } from './MessageClockCircle'
export { default as Target04 } from './Target04'
export { default as Tool03 } from './Tool03'

View File

@@ -0,0 +1,84 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import cn from 'classnames'
import s from './style.module.css'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type Props = {
srcs: string[]
}
const getWidthStyle = (imgNum: number) => {
if (imgNum === 1) {
return {
maxWidth: '100%',
}
}
if (imgNum === 2 || imgNum === 4) {
return {
width: 'calc(50% - 4px)',
}
}
return {
width: 'calc(33.3333% - 5.3333px)',
}
}
const ImageGallery: FC<Props> = ({
srcs,
}) => {
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const imgNum = srcs.length
const imgStyle = getWidthStyle(imgNum)
return (
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
{/* TODO: support preview */}
{srcs.map((src, index) => (
// eslint-disable-next-line @next/next/no-img-element
<img
key={index}
className={s.item}
style={imgStyle}
src={src}
alt=''
onClick={() => setImagePreviewUrl(src)}
/>
))}
{
imagePreviewUrl && (
<ImagePreview
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
/>
)
}
</div>
)
}
export default React.memo(ImageGallery)
export const ImageGalleryTest = () => {
const imgGallerySrcs = (() => {
const srcs = []
for (let i = 0; i < 6; i++)
// srcs.push('https://placekitten.com/640/360')
// srcs.push('https://placekitten.com/360/640')
srcs.push('https://placekitten.com/360/360')
return srcs
})()
return (
<div className='space-y-2'>
{imgGallerySrcs.map((_, index) => (
<div key={index} className='p-4 pb-2 rounded-lg bg-[#D1E9FF80]'>
<ImageGallery srcs={imgGallerySrcs.slice(0, index + 1)} />
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,22 @@
.item {
height: 200px;
margin-right: 8px;
margin-bottom: 8px;
object-fit: cover;
object-position: center;
border-radius: 8px;
cursor: pointer;
}
.item:nth-child(3n) {
margin-right: 0;
}
.img-2 .item:nth-child(2n),
.img-4 .item:nth-child(2n) {
margin-right: 0;
}
.img-4 .item:nth-child(3n) {
margin-right: 8px;
}

View File

@@ -0,0 +1,150 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Uploader from './uploader'
import ImageLinkInput from './image-link-input'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { TransferMethod } from '@/types/app'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Upload03 } from '@/app/components/base/icons/src/vender/line/general'
import type { ImageFile, VisionSettings } from '@/types/app'
type UploadOnlyFromLocalProps = {
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
limit?: number
}
const UploadOnlyFromLocal: FC<UploadOnlyFromLocalProps> = ({
onUpload,
disabled,
limit,
}) => {
return (
<Uploader onUpload={onUpload} disabled={disabled} limit={limit}>
{
hovering => (
<div className={`
relative flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer
${hovering && 'bg-gray-100'}
`}>
<ImagePlus className='w-4 h-4 text-gray-500' />
</div>
)
}
</Uploader>
)
}
type UploaderButtonProps = {
methods: VisionSettings['transfer_methods']
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
limit?: number
}
const UploaderButton: FC<UploaderButtonProps> = ({
methods,
onUpload,
disabled,
limit,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const hasUploadFromLocal = methods.find(method => method === TransferMethod.local_file)
const handleUpload = (imageFile: ImageFile) => {
setOpen(false)
onUpload(imageFile)
}
const handleToggle = () => {
if (disabled)
return
setOpen(v => !v)
}
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='top-start'
>
<PortalToFollowElemTrigger onClick={handleToggle}>
<div className={`
relative flex items-center justify-center w-8 h-8 hover:bg-gray-100 rounded-lg
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`}>
<ImagePlus className='w-4 h-4 text-gray-500' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='p-2 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
<ImageLinkInput onUpload={handleUpload} />
{
hasUploadFromLocal && (
<>
<div className='flex items-center mt-2 px-2 text-xs font-medium text-gray-400'>
<div className='mr-3 w-[93px] h-[1px] bg-gradient-to-l from-[#F3F4F6]' />
OR
<div className='ml-3 w-[93px] h-[1px] bg-gradient-to-r from-[#F3F4F6]' />
</div>
<Uploader onUpload={handleUpload} limit={limit}>
{
hovering => (
<div className={`
flex items-center justify-center h-8 text-[13px] font-medium text-[#155EEF] rounded-lg cursor-pointer
${hovering && 'bg-primary-50'}
`}>
<Upload03 className='mr-1 w-4 h-4' />
{t('common.imageUploader.uploadFromComputer')}
</div>
)
}
</Uploader>
</>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
type ChatImageUploaderProps = {
settings: VisionSettings
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
}
const ChatImageUploader: FC<ChatImageUploaderProps> = ({
settings,
onUpload,
disabled,
}) => {
const onlyUploadLocal = settings.transfer_methods.length === 1 && settings.transfer_methods[0] === TransferMethod.local_file
if (onlyUploadLocal) {
return (
<UploadOnlyFromLocal
onUpload={onUpload}
disabled={disabled}
limit={+settings.image_file_size_limit!}
/>
)
}
return (
<UploaderButton
methods={settings.transfer_methods}
onUpload={onUpload}
disabled={disabled}
limit={+settings.image_file_size_limit!}
/>
)
}
export default ChatImageUploader

View File

@@ -0,0 +1,81 @@
import { useState } from 'react'
import { useParams } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { imageUpload } from './utils'
import { useToastContext } from '@/app/components/base/toast'
import type { ImageFile } from '@/types/app'
export const useImageFiles = () => {
const params = useParams()
const { t } = useTranslation()
const { notify } = useToastContext()
const [files, setFiles] = useState<ImageFile[]>([])
const handleUpload = (imageFile: ImageFile) => {
const newFiles = [...files, imageFile]
setFiles(newFiles)
}
const handleRemove = (imageFileId: string) => {
const index = files.findIndex(file => file._id === imageFileId)
if (index > -1) {
const newFiles = [...files.slice(0, index), ...files.slice(index + 1)]
setFiles(newFiles)
}
}
const handleImageLinkLoadError = (imageFileId: string) => {
const index = files.findIndex(file => file._id === imageFileId)
if (index > -1) {
const currentFile = files[index]
const newFiles = [...files.slice(0, index), { ...currentFile, progress: -1 }, ...files.slice(index + 1)]
setFiles(newFiles)
}
}
const handleImageLinkLoadSuccess = (imageFileId: string) => {
const index = files.findIndex(file => file._id === imageFileId)
if (index > -1) {
const currentImageFile = files[index]
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: 100 }, ...files.slice(index + 1)]
setFiles(newFiles)
}
}
const handleReUpload = (imageFileId: string) => {
const index = files.findIndex(file => file._id === imageFileId)
if (index > -1) {
const currentImageFile = files[index]
imageUpload({
file: currentImageFile.file!,
onProgressCallback: (progress) => {
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress }, ...files.slice(index + 1)]
setFiles(newFiles)
},
onSuccessCallback: (res) => {
const newFiles = [...files.slice(0, index), { ...currentImageFile, fileId: res.id, progress: 100 }, ...files.slice(index + 1)]
setFiles(newFiles)
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
setFiles(newFiles)
},
}, !!params.token)
}
}
const handleClear = () => {
setFiles([])
}
return {
files,
onUpload: handleUpload,
onRemove: handleRemove,
onImageLinkLoadError: handleImageLinkLoadError,
onImageLinkLoadSuccess: handleImageLinkLoadSuccess,
onReUpload: handleReUpload,
onClear: handleClear,
}
}

View File

@@ -0,0 +1,50 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
type ImageLinkInputProps = {
onUpload: (imageFile: ImageFile) => void
}
const regex = /^(https?|ftp):\/\//
const ImageLinkInput: FC<ImageLinkInputProps> = ({
onUpload,
}) => {
const { t } = useTranslation()
const [imageLink, setImageLink] = useState('')
const handleClick = () => {
const imageFile = {
type: TransferMethod.remote_url,
_id: `${Date.now()}`,
fileId: '',
progress: regex.test(imageLink) ? 0 : -1,
url: imageLink,
}
onUpload(imageFile)
}
return (
<div className='flex items-center pl-1.5 pr-1 h-8 border border-gray-200 bg-white shadow-xs rounded-lg'>
<input
className='grow mr-0.5 px-1 h-[18px] text-[13px] outline-none appearance-none'
value={imageLink}
onChange={e => setImageLink(e.target.value)}
placeholder={t('common.imageUploader.pasteImageLinkInputPlaceholder') || ''}
/>
<Button
type='primary'
className='!h-6 text-xs font-medium'
disabled={!imageLink}
onClick={handleClick}
>
{t('common.operation.ok')}
</Button>
</div>
)
}
export default ImageLinkInput

View File

@@ -0,0 +1,129 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Loading02, XClose } from '@/app/components/base/icons/src/vender/line/general'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type ImageListProps = {
list: ImageFile[]
readonly?: boolean
onRemove?: (imageFileId: string) => void
onReUpload?: (imageFileId: string) => void
onImageLinkLoadSuccess?: (imageFileId: string) => void
onImageLinkLoadError?: (imageFileId: string) => void
}
const ImageList: FC<ImageListProps> = ({
list,
readonly,
onRemove,
onReUpload,
onImageLinkLoadSuccess,
onImageLinkLoadError,
}) => {
const { t } = useTranslation()
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const handleImageLinkLoadSuccess = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1)
onImageLinkLoadSuccess(item._id)
}
const handleImageLinkLoadError = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadError)
onImageLinkLoadError(item._id)
}
return (
<div className='flex flex-wrap'>
{
list.map(item => (
<div
key={item._id}
className='group relative mr-1 border-[0.5px] border-black/5 rounded-lg'
>
{
item.type === TransferMethod.local_file && item.progress !== 100 && (
<>
<div
className='absolute inset-0 flex items-center justify-center z-[1] bg-black/30'
style={{ left: item.progress > -1 ? `${item.progress}%` : 0 }}
>
{
item.progress === -1 && (
<RefreshCcw01 className='w-5 h-5 text-white' onClick={() => onReUpload && onReUpload(item._id)} />
)
}
</div>
{
item.progress > -1 && (
<span className='absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] text-sm text-white mix-blend-lighten z-[1]'>{item.progress}%</span>
)
}
</>
)
}
{
item.type === TransferMethod.remote_url && item.progress !== 100 && (
<div className={`
absolute inset-0 flex items-center justify-center rounded-lg z-[1] border
${item.progress === -1 ? 'bg-[#FEF0C7] border-[#DC6803]' : 'bg-black/[0.16] border-transparent'}
`}>
{
item.progress > -1 && (
<Loading02 className='animate-spin w-5 h-5 text-white' />
)
}
{
item.progress === -1 && (
<TooltipPlus popupContent={t('common.imageUploader.pasteImageLinkInvalid')}>
<AlertTriangle className='w-4 h-4 text-[#DC6803]' />
</TooltipPlus>
)
}
</div>
)
}
<img
className='w-16 h-16 rounded-lg object-cover cursor-pointer border-[0.5px] border-black/5'
alt=''
onLoad={() => handleImageLinkLoadSuccess(item)}
onError={() => handleImageLinkLoadError(item)}
src={item.type === TransferMethod.remote_url ? item.url : item.base64Url}
onClick={() => item.progress === 100 && setImagePreviewUrl((item.type === TransferMethod.remote_url ? item.url : item.base64Url) as string)}
/>
{
!readonly && (
<div
className={`
absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]
bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg
cursor-pointer
${item.progress === -1 ? 'flex' : 'hidden group-hover:flex'}
`}
onClick={() => onRemove && onRemove(item._id)}
>
<XClose className='w-3 h-3 text-gray-500' />
</div>
)
}
</div>
))
}
{
imagePreviewUrl && (
<ImagePreview
url={imagePreviewUrl}
onCancel={() => setImagePreviewUrl('')}
/>
)
}
</div>
)
}
export default ImageList

View File

@@ -0,0 +1,31 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
type ImagePreviewProps = {
url: string
onCancel: () => void
}
const ImagePreview: FC<ImagePreviewProps> = ({
url,
onCancel,
}) => {
return createPortal(
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
<img
alt='preview image'
src={url}
className='max-w-full max-h-full'
/>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}
>
<XClose className='w-4 h-4 text-white' />
</div>
</div>,
document.body,
)
}
export default ImagePreview

View File

@@ -0,0 +1,148 @@
import type { FC } from 'react'
import {
Fragment,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import Uploader from './uploader'
import ImageLinkInput from './image-link-input'
import ImageList from './image-list'
import { useImageFiles } from './hooks'
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
import { Link03 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { ImageFile, VisionSettings } from '@/types/app'
import { TransferMethod } from '@/types/app'
type PasteImageLinkButtonProps = {
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
}
const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
onUpload,
disabled,
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleUpload = (imageFile: ImageFile) => {
setOpen(false)
onUpload(imageFile)
}
const handleToggle = () => {
if (disabled)
return
setOpen(v => !v)
}
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='top-start'
>
<PortalToFollowElemTrigger onClick={handleToggle}>
<div className={`
relative flex items-center justify-center px-3 h-8 bg-gray-100 hover:bg-gray-200 text-xs text-gray-500 rounded-lg
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`}>
<Link03 className='mr-2 w-4 h-4' />
{t('common.imageUploader.pasteImageLink')}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='p-2 w-[320px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg'>
<ImageLinkInput onUpload={handleUpload} />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
type TextGenerationImageUploaderProps = {
settings: VisionSettings
onFilesChange: (files: ImageFile[]) => void
}
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
settings,
onFilesChange,
}) => {
const { t } = useTranslation()
const {
files,
onUpload,
onRemove,
onImageLinkLoadError,
onImageLinkLoadSuccess,
onReUpload,
} = useImageFiles()
useEffect(() => {
onFilesChange(files)
}, [files])
const localUpload = (
<Uploader
onUpload={onUpload}
disabled={files.length >= settings.number_limits}
limit={+settings.image_file_size_limit!}
>
{
hovering => (
<div className={`
flex items-center justify-center px-3 h-8 bg-gray-100
text-xs text-gray-500 rounded-lg cursor-pointer
${hovering && 'bg-gray-200'}
`}>
<ImagePlus className='mr-2 w-4 h-4' />
{t('common.imageUploader.uploadFromComputer')}
</div>
)
}
</Uploader>
)
const urlUpload = (
<PasteImageLinkButton
onUpload={onUpload}
disabled={files.length >= settings.number_limits}
/>
)
return (
<div>
<div className='mb-1'>
<ImageList
list={files}
onRemove={onRemove}
onReUpload={onReUpload}
onImageLinkLoadError={onImageLinkLoadError}
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
/>
</div>
<div className={`grid gap-1 ${settings.transfer_methods.length === 2 ? 'grid-cols-2' : 'grid-cols-1'}`}>
{
settings.transfer_methods.map((method) => {
if (method === TransferMethod.local_file)
return <Fragment key={TransferMethod.local_file}>{localUpload}</Fragment>
if (method === TransferMethod.remote_url)
return <Fragment key={TransferMethod.remote_url}>{urlUpload}</Fragment>
return null
})
}
</div>
</div>
)
}
export default TextGenerationImageUploader

View File

@@ -0,0 +1,100 @@
import type { ChangeEvent, FC } from 'react'
import { useState } from 'react'
import { useParams } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { imageUpload } from './utils'
import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast'
type UploaderProps = {
children: (hovering: boolean) => JSX.Element
onUpload: (imageFile: ImageFile) => void
limit?: number
disabled?: boolean
}
const Uploader: FC<UploaderProps> = ({
children,
onUpload,
limit,
disabled,
}) => {
const [hovering, setHovering] = useState(false)
const params = useParams()
const { notify } = useToastContext()
const { t } = useTranslation()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
if (limit && file.size > limit * 1024 * 1024) {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) })
return
}
const reader = new FileReader()
reader.addEventListener(
'load',
() => {
const imageFile = {
type: TransferMethod.local_file,
_id: `${Date.now()}`,
fileId: '',
file,
url: reader.result as string,
base64Url: reader.result as string,
progress: 0,
}
onUpload(imageFile)
imageUpload({
file: imageFile.file,
onProgressCallback: (progress) => {
onUpload({ ...imageFile, progress })
},
onSuccessCallback: (res) => {
onUpload({ ...imageFile, fileId: res.id, progress: 100 })
},
onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
onUpload({ ...imageFile, progress: -1 })
},
}, !!params.token)
},
false,
)
reader.addEventListener(
'error',
() => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') })
},
false,
)
reader.readAsDataURL(file)
}
return (
<div
className='relative'
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
{children(hovering)}
<input
className={`
absolute block inset-0 opacity-0 text-[0]
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`}
type='file'
accept='.png, .jpg, .jpeg, .webp, .gif'
onChange={handleChange}
disabled={disabled}
/>
</div>
)
}
export default Uploader

View File

@@ -0,0 +1,36 @@
import { upload } from '@/service/base'
type ImageUploadParams = {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void
onErrorCallback: () => void
}
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean) => void
export const imageUpload: ImageUpload = ({
file,
onProgressCallback,
onSuccessCallback,
onErrorCallback,
}, isPublic) => {
const formData = new FormData()
formData.append('file', file)
const onProgress = (e: ProgressEvent) => {
if (e.lengthComputable) {
const percent = Math.floor(e.loaded / e.total * 100)
onProgressCallback(percent)
}
}
upload({
xhr: new XMLHttpRequest(),
data: formData,
onprogress: onProgress,
}, isPublic)
.then((res: { id: string }) => {
onSuccessCallback(res)
})
.catch(() => {
onErrorCallback()
})
}

View File

@@ -0,0 +1,73 @@
'use client'
import type { FC } from 'react'
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import Tooltip from '@/app/components/base/tooltip-plus'
import Slider from '@/app/components/base/slider'
import Switch from '@/app/components/base/switch'
type Props = {
className?: string
id: string
name: string
noTooltip?: boolean
tip?: string
value: number
enable: boolean
step?: number
min?: number
max: number
onChange: (key: string, value: number) => void
hasSwitch?: boolean
onSwitchChange?: (key: string, enable: boolean) => void
}
const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1, min = 0, max, value, enable, onChange, hasSwitch, onSwitchChange }) => {
return (
<div className={className}>
<div className="flex items-center h-8 justify-between">
<div className="flex items-center">
{hasSwitch && (
<Switch
size='md'
defaultValue={enable}
onChange={async (val) => {
onSwitchChange?.(id, val)
}}
/>
)}
<span className="mx-1 text-gray-900 text-[13px] leading-[18px] font-medium">{name}</span>
{!noTooltip && (
<Tooltip popupContent={<div className="w-[200px]">{tip}</div>}>
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
</Tooltip>
)}
</div>
<div className="flex items-center"></div>
</div>
<div className="mt-2 flex items-center">
<div className="mr-4 flex shrink-0 items-center">
<input disabled={!enable} type="number" min={min} max={max} step={step} className="block w-[48px] h-7 text-xs leading-[18px] rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600 disabled:opacity-60" value={value} onChange={(e) => {
const value = parseFloat(e.target.value)
if (value < min || value > max)
return
onChange(id, value)
}} />
</div>
<div className="flex items-center h-7 grow">
<Slider
className='w-full'
disabled={!enable}
value={max < 5 ? value * 100 : value}
min={min < 1 ? min * 100 : min}
max={max < 5 ? max * 100 : max}
onChange={value => onChange(id, value / (max < 5 ? 100 : 1))}
/>
</div>
</div>
</div>
)
}
export default ParamItem

View File

@@ -0,0 +1,54 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ParamItem from '.'
type Props = {
className?: string
value: number
onChange: (key: string, value: number) => void
enable: boolean
hasSwitch?: boolean
onSwitchChange?: (key: string, enable: boolean) => void
}
const VALUE_LIMIT = {
default: 0.7,
step: 0.01,
min: 0,
max: 1,
}
const key = 'score_threshold'
const ScoreThresholdItem: FC<Props> = ({
className,
value,
enable,
onChange,
hasSwitch,
onSwitchChange,
}) => {
const { t } = useTranslation()
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)
}
return (
<ParamItem
className={className}
id={key}
name={t(`appDebug.datasetConfig.${key}`)}
tip={t(`appDebug.datasetConfig.${key}Tip`)}
{...VALUE_LIMIT}
value={value}
enable={enable}
onChange={handleParamChange}
hasSwitch={hasSwitch}
onSwitchChange={onSwitchChange}
/>
)
}
export default React.memo(ScoreThresholdItem)

View File

@@ -0,0 +1,48 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ParamItem from '.'
type Props = {
className?: string
value: number
onChange: (key: string, value: number) => void
enable: boolean
}
const VALUE_LIMIT = {
default: 2,
step: 1,
min: 1,
max: 10,
}
const key = 'top_k'
const TopKItem: FC<Props> = ({
className,
value,
enable,
onChange,
}) => {
const { t } = useTranslation()
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)
}
return (
<ParamItem
className={className}
id={key}
name={t(`appDebug.datasetConfig.${key}`)}
tip={t(`appDebug.datasetConfig.${key}Tip`)}
{...VALUE_LIMIT}
value={value}
enable={enable}
onChange={handleParamChange}
/>
)
}
export default React.memo(TopKItem)

View File

@@ -1,6 +1,8 @@
import ReactSlider from 'react-slider'
import cn from 'classnames'
import './style.css'
type ISliderProps = {
className?: string
value: number
max?: number
min?: number
@@ -9,14 +11,14 @@ type ISliderProps = {
onChange: (value: number) => void
}
const Slider: React.FC<ISliderProps> = ({ max, min, step, value, disabled, onChange }) => {
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
return <ReactSlider
disabled={disabled}
value={isNaN(value) ? 0 : value}
min={min || 0}
max={max || 100}
step={step || 1}
className="slider"
className={cn(className, 'slider')}
thumbClassName="slider-thumb"
trackClassName="slider-track"
onChange={onChange}