Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
W
webapp-conversation
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
webapp-conversation
Commits
5dc3658e
Commit
5dc3658e
authored
Nov 22, 2023
by
StyleZhang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support upload image
parent
ae70d2fe
Changes
37
Show whitespace changes
Inline
Side-by-side
Showing
37 changed files
with
1674 additions
and
33 deletions
+1674
-33
route.ts
app/api/chat-messages/route.ts
+2
-1
route.ts
app/api/file-upload/route.ts
+15
-0
IconBase.tsx
app/components/base/icons/IconBase.tsx
+31
-0
data.json
app/components/base/icons/line/image-plus/data.json
+39
-0
index.tsx
app/components/base/icons/line/image-plus/index.tsx
+13
-0
data.json
app/components/base/icons/line/link-03/data.json
+57
-0
index.tsx
app/components/base/icons/line/link-03/index.tsx
+13
-0
data.json
app/components/base/icons/line/loading-02/data.json
+64
-0
index.tsx
app/components/base/icons/line/loading-02/index.tsx
+13
-0
data.json
app/components/base/icons/line/refresh-ccw-01/data.json
+29
-0
index.tsx
app/components/base/icons/line/refresh-ccw-01/index.tsx
+13
-0
data.json
app/components/base/icons/line/upload-03/data.json
+66
-0
index.tsx
app/components/base/icons/line/upload-03/index.tsx
+13
-0
data.json
app/components/base/icons/line/x-close/data.json
+39
-0
index.tsx
app/components/base/icons/line/x-close/index.tsx
+13
-0
data.json
app/components/base/icons/solid/alert-triangle/data.json
+38
-0
index.tsx
app/components/base/icons/solid/alert-triangle/index.tsx
+13
-0
utils.tsx
app/components/base/icons/utils.tsx
+66
-0
index.tsx
app/components/base/image-gallery/index.tsx
+83
-0
style.module.css
app/components/base/image-gallery/style.module.css
+22
-0
chat-image-uploader.tsx
app/components/base/image-uploader/chat-image-uploader.tsx
+150
-0
hooks.ts
app/components/base/image-uploader/hooks.ts
+108
-0
image-link-input.tsx
app/components/base/image-uploader/image-link-input.tsx
+50
-0
image-list.tsx
app/components/base/image-uploader/image-list.tsx
+130
-0
image-preview.tsx
app/components/base/image-uploader/image-preview.tsx
+31
-0
uploader.tsx
app/components/base/image-uploader/uploader.tsx
+101
-0
utils.ts
app/components/base/image-uploader/utils.ts
+38
-0
index.tsx
app/components/base/portal-to-follow-elem/index.tsx
+167
-0
index.tsx
app/components/base/toast/index.tsx
+3
-1
index.tsx
app/components/base/tooltip-plus/index.tsx
+50
-0
index.tsx
app/components/chat/index.tsx
+81
-20
index.tsx
app/components/index.tsx
+25
-9
common.en.ts
i18n/lang/common.en.ts
+11
-0
common.zh.ts
i18n/lang/common.zh.ts
+11
-0
package.json
package.json
+3
-1
base.ts
service/base.ts
+34
-1
app.ts
types/app.ts
+39
-0
No files found.
app/api/chat-messages/route.ts
View file @
5dc3658e
...
@@ -6,10 +6,11 @@ export async function POST(request: NextRequest) {
...
@@ -6,10 +6,11 @@ export async function POST(request: NextRequest) {
const
{
const
{
inputs
,
inputs
,
query
,
query
,
files
,
conversation_id
:
conversationId
,
conversation_id
:
conversationId
,
response_mode
:
responseMode
,
response_mode
:
responseMode
,
}
=
body
}
=
body
const
{
user
}
=
getInfo
(
request
)
const
{
user
}
=
getInfo
(
request
)
const
res
=
await
client
.
createChatMessage
(
inputs
,
query
,
user
,
responseMode
,
conversationId
)
const
res
=
await
client
.
createChatMessage
(
inputs
,
query
,
user
,
responseMode
,
conversationId
,
files
)
return
new
Response
(
res
.
data
as
any
)
return
new
Response
(
res
.
data
as
any
)
}
}
app/api/file-upload/route.ts
0 → 100644
View file @
5dc3658e
import
{
type
NextRequest
}
from
'next/server'
import
{
client
,
getInfo
}
from
'@/app/api/utils/common'
export
async
function
POST
(
request
:
NextRequest
)
{
try
{
const
formData
=
await
request
.
formData
()
const
{
user
}
=
getInfo
(
request
)
formData
.
append
(
'user'
,
user
)
const
res
=
await
client
.
fileUpload
(
formData
)
return
new
Response
(
res
.
data
.
id
as
any
)
}
catch
(
e
:
any
)
{
return
new
Response
(
e
.
message
)
}
}
app/components/base/icons/IconBase.tsx
0 → 100644
View file @
5dc3658e
import
{
forwardRef
}
from
'react'
import
{
generate
}
from
'./utils'
import
type
{
AbstractNode
}
from
'./utils'
export
type
IconData
=
{
name
:
string
icon
:
AbstractNode
}
export
type
IconBaseProps
=
{
data
:
IconData
className
?:
string
onClick
?:
React
.
MouseEventHandler
<
SVGElement
>
style
?:
React
.
CSSProperties
}
const
IconBase
=
forwardRef
<
React
.
MutableRefObject
<
HTMLOrSVGElement
>
,
IconBaseProps
>
((
props
,
ref
)
=>
{
const
{
data
,
className
,
onClick
,
style
,
...
restProps
}
=
props
return
generate
(
data
.
icon
,
`svg-
${
data
.
name
}
`
,
{
className
,
onClick
,
style
,
'data-icon'
:
data
.
name
,
'aria-hidden'
:
'true'
,
...
restProps
,
'ref'
:
ref
,
})
})
export
default
IconBase
app/components/base/icons/line/image-plus/data.json
0 → 100644
View file @
5dc3658e
{
"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"
}
\ No newline at end of file
app/components/base/icons/line/image-plus/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
app/components/base/icons/line/link-03/data.json
0 → 100644
View file @
5dc3658e
{
"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"
}
\ No newline at end of file
app/components/base/icons/line/link-03/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
app/components/base/icons/line/loading-02/data.json
0 → 100644
View file @
5dc3658e
{
"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"
:
{
"clip-path"
:
"url(#clip0_6037_51601)"
},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"path"
,
"attributes"
:
{
"d"
:
"M7.99992 1.33398V4.00065M7.99992 12.0007V14.6673M3.99992 8.00065H1.33325M14.6666 8.00065H11.9999M12.7189 12.7196L10.8333 10.834M12.7189 3.33395L10.8333 5.21956M3.28097 12.7196L5.16659 10.834M3.28097 3.33395L5.16659 5.21956"
,
"stroke"
:
"currentColor"
,
"stroke-width"
:
"1.25"
,
"stroke-linecap"
:
"round"
,
"stroke-linejoin"
:
"round"
},
"children"
:
[]
}
]
},
{
"type"
:
"element"
,
"name"
:
"defs"
,
"attributes"
:
{},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"clipPath"
,
"attributes"
:
{
"id"
:
"clip0_6037_51601"
},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"rect"
,
"attributes"
:
{
"width"
:
"16"
,
"height"
:
"16"
,
"fill"
:
"white"
},
"children"
:
[]
}
]
}
]
}
]
},
"name"
:
"Loading02"
}
\ No newline at end of file
app/components/base/icons/line/loading-02/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
=
'Loading02'
export
default
Icon
app/components/base/icons/line/refresh-ccw-01/data.json
0 → 100644
View file @
5dc3658e
{
"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"
:
"M2 10C2 10 4.00498 7.26822 5.63384 5.63824C7.26269 4.00827 9.5136 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.89691 21 4.43511 18.2543 3.35177 14.5M2 10V4M2 10H8"
,
"stroke"
:
"currentColor"
,
"stroke-width"
:
"2"
,
"stroke-linecap"
:
"round"
,
"stroke-linejoin"
:
"round"
},
"children"
:
[]
}
]
},
"name"
:
"RefreshCcw01"
}
\ No newline at end of file
app/components/base/icons/line/refresh-ccw-01/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
=
'RefreshCcw01'
export
default
Icon
app/components/base/icons/line/upload-03/data.json
0 → 100644
View file @
5dc3658e
{
"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"
}
\ No newline at end of file
app/components/base/icons/line/upload-03/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
app/components/base/icons/line/x-close/data.json
0 → 100644
View file @
5dc3658e
{
"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"
:
"x-close"
},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"path"
,
"attributes"
:
{
"id"
:
"Icon"
,
"d"
:
"M12 4L4 12M4 4L12 12"
,
"stroke"
:
"currentColor"
,
"stroke-width"
:
"1.25"
,
"stroke-linecap"
:
"round"
,
"stroke-linejoin"
:
"round"
},
"children"
:
[]
}
]
}
]
},
"name"
:
"XClose"
}
\ No newline at end of file
app/components/base/icons/line/x-close/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
=
'XClose'
export
default
Icon
app/components/base/icons/solid/alert-triangle/data.json
0 → 100644
View file @
5dc3658e
{
"icon"
:
{
"type"
:
"element"
,
"isRootNode"
:
true
,
"name"
:
"svg"
,
"attributes"
:
{
"width"
:
"12"
,
"height"
:
"12"
,
"viewBox"
:
"0 0 12 12"
,
"fill"
:
"none"
,
"xmlns"
:
"http://www.w3.org/2000/svg"
},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"g"
,
"attributes"
:
{
"id"
:
"alert-triangle"
},
"children"
:
[
{
"type"
:
"element"
,
"name"
:
"path"
,
"attributes"
:
{
"id"
:
"Solid"
,
"fill-rule"
:
"evenodd"
,
"clip-rule"
:
"evenodd"
,
"d"
:
"M6.40616 0.834185C6.14751 0.719172 5.85222 0.719172 5.59356 0.834185C5.3938 0.923011 5.26403 1.07947 5.17373 1.20696C5.08495 1.3323 4.9899 1.49651 4.88536 1.67711L0.751783 8.81693C0.646828 8.99818 0.551451 9.16289 0.486781 9.30268C0.421056 9.44475 0.349754 9.63572 0.372478 9.85369C0.401884 10.1357 0.549654 10.392 0.779012 10.5588C0.956259 10.6877 1.15726 10.7217 1.31314 10.736C1.46651 10.75 1.65684 10.75 1.86628 10.75H10.1334C10.3429 10.75 10.5332 10.75 10.6866 10.736C10.8425 10.7217 11.0435 10.6877 11.2207 10.5588C11.4501 10.392 11.5978 10.1357 11.6272 9.85369C11.65 9.63572 11.5787 9.44475 11.5129 9.30268C11.4483 9.1629 11.3529 8.9982 11.248 8.81697L7.11436 1.67709C7.00983 1.49651 6.91477 1.3323 6.82599 1.20696C6.73569 1.07947 6.60593 0.923011 6.40616 0.834185ZM6.49988 4.5C6.49988 4.22386 6.27602 4 5.99988 4C5.72374 4 5.49988 4.22386 5.49988 4.5V6.5C5.49988 6.77614 5.72374 7 5.99988 7C6.27602 7 6.49988 6.77614 6.49988 6.5V4.5ZM5.99988 8C5.72374 8 5.49988 8.22386 5.49988 8.5C5.49988 8.77614 5.72374 9 5.99988 9H6.00488C6.28102 9 6.50488 8.77614 6.50488 8.5C6.50488 8.22386 6.28102 8 6.00488 8H5.99988Z"
,
"fill"
:
"currentColor"
},
"children"
:
[]
}
]
}
]
},
"name"
:
"AlertTriangle"
}
\ No newline at end of file
app/components/base/icons/solid/alert-triangle/index.tsx
0 → 100644
View file @
5dc3658e
import
*
as
React
from
'react'
import
data
from
'./data.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
=
'AlertTriangle'
export
default
Icon
app/components/base/icons/utils.tsx
0 → 100644
View file @
5dc3658e
import
React
from
'react'
export
type
AbstractNode
=
{
name
:
string
attributes
:
{
[
key
:
string
]:
string
}
children
?:
AbstractNode
[]
}
export
type
Attrs
=
{
[
key
:
string
]:
string
}
export
function
normalizeAttrs
(
attrs
:
Attrs
=
{}):
Attrs
{
return
Object
.
keys
(
attrs
).
reduce
((
acc
:
Attrs
,
key
)
=>
{
const
val
=
attrs
[
key
]
key
=
key
.
replace
(
/
([
-
]\w)
/g
,
(
g
:
string
)
=>
g
[
1
].
toUpperCase
())
key
=
key
.
replace
(
/
([
:
]\w)
/g
,
(
g
:
string
)
=>
g
[
1
].
toUpperCase
())
switch
(
key
)
{
case
'class'
:
acc
.
className
=
val
delete
acc
.
class
break
case
'style'
:
(
acc
.
style
as
any
)
=
val
.
split
(
';'
).
reduce
((
prev
,
next
)
=>
{
const
pairs
=
next
?.
split
(
':'
)
if
(
pairs
[
0
]
&&
pairs
[
1
])
{
const
k
=
pairs
[
0
].
replace
(
/
([
-
]\w)
/g
,
(
g
:
string
)
=>
g
[
1
].
toUpperCase
())
prev
[
k
]
=
pairs
[
1
]
}
return
prev
},
{}
as
Attrs
)
break
default
:
acc
[
key
]
=
val
}
return
acc
},
{})
}
export
function
generate
(
node
:
AbstractNode
,
key
:
string
,
rootProps
?:
{
[
key
:
string
]:
any
}
|
false
,
):
any
{
if
(
!
rootProps
)
{
return
React
.
createElement
(
node
.
name
,
{
key
,
...
normalizeAttrs
(
node
.
attributes
)
},
(
node
.
children
||
[]).
map
((
child
,
index
)
=>
generate
(
child
,
`
${
key
}
-
${
node
.
name
}
-
${
index
}
`
)),
)
}
return
React
.
createElement
(
node
.
name
,
{
key
,
...
normalizeAttrs
(
node
.
attributes
),
...
rootProps
,
},
(
node
.
children
||
[]).
map
((
child
,
index
)
=>
generate
(
child
,
`
${
key
}
-
${
node
.
name
}
-
${
index
}
`
)),
)
}
app/components/base/image-gallery/index.tsx
0 → 100644
View file @
5dc3658e
'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
)
=>
(
<
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
>
)
}
app/components/base/image-gallery/style.module.css
0 → 100644
View file @
5dc3658e
.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
;
}
\ No newline at end of file
app/components/base/image-uploader/chat-image-uploader.tsx
0 → 100644
View file @
5dc3658e
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/line/image-plus'
import
{
TransferMethod
}
from
'@/types/app'
import
{
PortalToFollowElem
,
PortalToFollowElemContent
,
PortalToFollowElemTrigger
,
}
from
'@/app/components/base/portal-to-follow-elem'
import
Upload03
from
'@/app/components/base/icons/line/upload-03'
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
app/components/base/image-uploader/hooks.ts
0 → 100644
View file @
5dc3658e
import
{
useMemo
,
useRef
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
imageUpload
}
from
'./utils'
import
Toast
from
'@/app/components/base/toast'
import
type
{
ImageFile
}
from
'@/types/app'
export
const
useImageFiles
=
()
=>
{
const
{
t
}
=
useTranslation
()
const
{
notify
}
=
Toast
const
[
files
,
setFiles
]
=
useState
<
ImageFile
[]
>
([])
const
filesRef
=
useRef
<
ImageFile
[]
>
([])
const
handleUpload
=
(
imageFile
:
ImageFile
)
=>
{
const
files
=
filesRef
.
current
const
index
=
files
.
findIndex
(
file
=>
file
.
_id
===
imageFile
.
_id
)
if
(
index
>
-
1
)
{
const
currentFile
=
files
[
index
]
const
newFiles
=
[...
files
.
slice
(
0
,
index
),
{
...
currentFile
,
...
imageFile
},
...
files
.
slice
(
index
+
1
)]
setFiles
(
newFiles
)
filesRef
.
current
=
newFiles
}
else
{
const
newFiles
=
[...
files
,
imageFile
]
setFiles
(
newFiles
)
filesRef
.
current
=
newFiles
}
}
const
handleRemove
=
(
imageFileId
:
string
)
=>
{
const
files
=
filesRef
.
current
const
index
=
files
.
findIndex
(
file
=>
file
.
_id
===
imageFileId
)
if
(
index
>
-
1
)
{
const
currentFile
=
files
[
index
]
const
newFiles
=
[...
files
.
slice
(
0
,
index
),
{
...
currentFile
,
deleted
:
true
},
...
files
.
slice
(
index
+
1
)]
setFiles
(
newFiles
)
filesRef
.
current
=
newFiles
}
}
const
handleImageLinkLoadError
=
(
imageFileId
:
string
)
=>
{
const
files
=
filesRef
.
current
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
)]
filesRef
.
current
=
newFiles
setFiles
(
newFiles
)
}
}
const
handleImageLinkLoadSuccess
=
(
imageFileId
:
string
)
=>
{
const
files
=
filesRef
.
current
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
)]
filesRef
.
current
=
newFiles
setFiles
(
newFiles
)
}
}
const
handleReUpload
=
(
imageFileId
:
string
)
=>
{
const
files
=
filesRef
.
current
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
)]
filesRef
.
current
=
newFiles
setFiles
(
newFiles
)
},
onSuccessCallback
:
(
res
)
=>
{
const
newFiles
=
[...
files
.
slice
(
0
,
index
),
{
...
currentImageFile
,
fileId
:
res
.
id
,
progress
:
100
},
...
files
.
slice
(
index
+
1
)]
filesRef
.
current
=
newFiles
setFiles
(
newFiles
)
},
onErrorCallback
:
()
=>
{
notify
({
type
:
'error'
,
message
:
t
(
'common.imageUploader.uploadFromComputerUploadError'
)
})
const
newFiles
=
[...
files
.
slice
(
0
,
index
),
{
...
currentImageFile
,
progress
:
-
1
},
...
files
.
slice
(
index
+
1
)]
filesRef
.
current
=
newFiles
setFiles
(
newFiles
)
},
})
}
}
const
handleClear
=
()
=>
{
setFiles
([])
filesRef
.
current
=
[]
}
const
filteredFiles
=
useMemo
(()
=>
{
return
files
.
filter
(
file
=>
!
file
.
deleted
)
},
[
files
])
return
{
files
:
filteredFiles
,
onUpload
:
handleUpload
,
onRemove
:
handleRemove
,
onImageLinkLoadError
:
handleImageLinkLoadError
,
onImageLinkLoadSuccess
:
handleImageLinkLoadSuccess
,
onReUpload
:
handleReUpload
,
onClear
:
handleClear
,
}
}
app/components/base/image-uploader/image-link-input.tsx
0 → 100644
View file @
5dc3658e
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
app/components/base/image-uploader/image-list.tsx
0 → 100644
View file @
5dc3658e
import
type
{
FC
}
from
'react'
import
{
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Loading02
from
'@/app/components/base/icons/line/loading-02'
import
XClose
from
'@/app/components/base/icons/line/x-close'
import
RefreshCcw01
from
'@/app/components/base/icons/line/refresh-ccw-01'
import
AlertTriangle
from
'@/app/components/base/icons/solid/alert-triangle'
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
app/components/base/image-uploader/image-preview.tsx
0 → 100644
View file @
5dc3658e
import
type
{
FC
}
from
'react'
import
{
createPortal
}
from
'react-dom'
import
XClose
from
'@/app/components/base/icons/line/x-close'
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
app/components/base/image-uploader/uploader.tsx
0 → 100644
View file @
5dc3658e
'use client'
import
type
{
ChangeEvent
,
FC
}
from
'react'
import
{
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
imageUpload
}
from
'./utils'
import
type
{
ImageFile
}
from
'@/types/app'
import
{
TransferMethod
}
from
'@/types/app'
import
Toast
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
{
notify
}
=
Toast
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
})
},
})
},
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] w-full
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
`
}
onClick=
{
e
=>
(
e
.
target
as
HTMLInputElement
).
value
=
''
}
type=
'file'
accept=
'.png, .jpg, .jpeg, .webp, .gif'
onChange=
{
handleChange
}
disabled=
{
disabled
}
/>
</
div
>
)
}
export
default
Uploader
app/components/base/image-uploader/utils.ts
0 → 100644
View file @
5dc3658e
'use client'
import
{
upload
}
from
'@/service/base'
type
ImageUploadParams
=
{
file
:
File
onProgressCallback
:
(
progress
:
number
)
=>
void
onSuccessCallback
:
(
res
:
{
id
:
string
})
=>
void
onErrorCallback
:
()
=>
void
}
type
ImageUpload
=
(
v
:
ImageUploadParams
)
=>
void
export
const
imageUpload
:
ImageUpload
=
({
file
,
onProgressCallback
,
onSuccessCallback
,
onErrorCallback
,
})
=>
{
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
,
})
.
then
((
res
:
{
id
:
string
})
=>
{
onSuccessCallback
(
res
)
})
.
catch
(()
=>
{
onErrorCallback
()
})
}
app/components/base/portal-to-follow-elem/index.tsx
0 → 100644
View file @
5dc3658e
'use client'
import
React
from
'react'
import
{
FloatingPortal
,
autoUpdate
,
flip
,
offset
,
shift
,
useDismiss
,
useFloating
,
useFocus
,
useHover
,
useInteractions
,
useMergeRefs
,
useRole
,
}
from
'@floating-ui/react'
import
type
{
OffsetOptions
,
Placement
}
from
'@floating-ui/react'
type
PortalToFollowElemOptions
=
{
/*
* top, bottom, left, right
* start, end. Default is middle
* combine: top-start, top-end
*/
placement
?:
Placement
open
?:
boolean
offset
?:
number
|
OffsetOptions
onOpenChange
?:
(
open
:
boolean
)
=>
void
}
export
function
usePortalToFollowElem
({
placement
=
'bottom'
,
open
,
offset
:
offsetValue
=
0
,
onOpenChange
:
setControlledOpen
,
}:
PortalToFollowElemOptions
=
{})
{
const
setOpen
=
setControlledOpen
const
data
=
useFloating
({
placement
,
open
,
onOpenChange
:
setOpen
,
whileElementsMounted
:
autoUpdate
,
middleware
:
[
offset
(
offsetValue
),
flip
({
crossAxis
:
placement
.
includes
(
'-'
),
fallbackAxisSideDirection
:
'start'
,
padding
:
5
,
}),
shift
({
padding
:
5
}),
],
})
const
context
=
data
.
context
const
hover
=
useHover
(
context
,
{
move
:
false
,
enabled
:
open
==
null
,
})
const
focus
=
useFocus
(
context
,
{
enabled
:
open
==
null
,
})
const
dismiss
=
useDismiss
(
context
)
const
role
=
useRole
(
context
,
{
role
:
'tooltip'
})
const
interactions
=
useInteractions
([
hover
,
focus
,
dismiss
,
role
])
return
React
.
useMemo
(
()
=>
({
open
,
setOpen
,
...
interactions
,
...
data
,
}),
[
open
,
setOpen
,
interactions
,
data
],
)
}
type
ContextType
=
ReturnType
<
typeof
usePortalToFollowElem
>
|
null
const
PortalToFollowElemContext
=
React
.
createContext
<
ContextType
>
(
null
)
export
function
usePortalToFollowElemContext
()
{
const
context
=
React
.
useContext
(
PortalToFollowElemContext
)
if
(
context
==
null
)
throw
new
Error
(
'PortalToFollowElem components must be wrapped in <PortalToFollowElem />'
)
return
context
}
export
function
PortalToFollowElem
({
children
,
...
options
}:
{
children
:
React
.
ReactNode
}
&
PortalToFollowElemOptions
)
{
// This can accept any props as options, e.g. `placement`,
// or other positioning options.
const
tooltip
=
usePortalToFollowElem
(
options
)
return
(
<
PortalToFollowElemContext
.
Provider
value=
{
tooltip
}
>
{
children
}
</
PortalToFollowElemContext
.
Provider
>
)
}
export
const
PortalToFollowElemTrigger
=
React
.
forwardRef
<
HTMLElement
,
React
.
HTMLProps
<
HTMLElement
>
&
{
asChild
?:
boolean
}
>
(({
children
,
asChild
=
false
,
...
props
},
propRef
)
=>
{
const
context
=
usePortalToFollowElemContext
()
const
childrenRef
=
(
children
as
any
).
ref
const
ref
=
useMergeRefs
([
context
.
refs
.
setReference
,
propRef
,
childrenRef
])
// `asChild` allows the user to pass any element as the anchor
if
(
asChild
&&
React
.
isValidElement
(
children
))
{
return
React
.
cloneElement
(
children
,
context
.
getReferenceProps
({
ref
,
...
props
,
...
children
.
props
,
'data-state'
:
context
.
open
?
'open'
:
'closed'
,
}),
)
}
return
(
<
div
ref=
{
ref
}
className=
'inline-block'
// The user can style the trigger based on the state
data
-
state=
{
context
.
open
?
'open'
:
'closed'
}
{
...
context
.
getReferenceProps
(
props
)}
>
{
children
}
</
div
>
)
})
PortalToFollowElemTrigger
.
displayName
=
'PortalToFollowElemTrigger'
export
const
PortalToFollowElemContent
=
React
.
forwardRef
<
HTMLDivElement
,
React
.
HTMLProps
<
HTMLDivElement
>
>
(({
style
,
...
props
},
propRef
)
=>
{
const
context
=
usePortalToFollowElemContext
()
const
ref
=
useMergeRefs
([
context
.
refs
.
setFloating
,
propRef
])
if
(
!
context
.
open
)
return
null
return
(
<
FloatingPortal
>
<
div
ref=
{
ref
}
style=
{
{
...
context
.
floatingStyles
,
...
style
,
}
}
{
...
context
.
getFloatingProps
(
props
)}
/>
</
FloatingPortal
>
)
})
PortalToFollowElemContent
.
displayName
=
'PortalToFollowElemContent'
app/components/base/toast/index.tsx
View file @
5dc3658e
...
@@ -9,7 +9,7 @@ import {
...
@@ -9,7 +9,7 @@ import {
InformationCircleIcon
,
InformationCircleIcon
,
XCircleIcon
,
XCircleIcon
,
}
from
'@heroicons/react/20/solid'
}
from
'@heroicons/react/20/solid'
import
{
createContext
}
from
'use-context-selector'
import
{
createContext
,
useContext
}
from
'use-context-selector'
export
type
IToastProps
=
{
export
type
IToastProps
=
{
type
?:
'success'
|
'error'
|
'warning'
|
'info'
type
?:
'success'
|
'error'
|
'warning'
|
'info'
...
@@ -24,6 +24,8 @@ type IToastContext = {
...
@@ -24,6 +24,8 @@ type IToastContext = {
const
defaultDuring
=
3000
const
defaultDuring
=
3000
export
const
ToastContext
=
createContext
<
IToastContext
>
({}
as
IToastContext
)
export
const
ToastContext
=
createContext
<
IToastContext
>
({}
as
IToastContext
)
export
const
useToastContext
=
()
=>
useContext
(
ToastContext
)
const
Toast
=
({
const
Toast
=
({
type
=
'info'
,
type
=
'info'
,
duration
,
duration
,
...
...
app/components/base/tooltip-plus/index.tsx
0 → 100644
View file @
5dc3658e
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useState
}
from
'react'
import
{
PortalToFollowElem
,
PortalToFollowElemContent
,
PortalToFollowElemTrigger
}
from
'@/app/components/base/portal-to-follow-elem'
export
type
TooltipProps
=
{
position
?:
'top'
|
'right'
|
'bottom'
|
'left'
triggerMethod
?:
'hover'
|
'click'
popupContent
:
React
.
ReactNode
children
:
React
.
ReactNode
}
const
arrow
=
(
<
svg
className=
"absolute text-white h-2 w-full left-0 top-full"
x=
"0px"
y=
"0px"
viewBox=
"0 0 255 255"
><
polygon
className=
"fill-current"
points=
"0,0 127.5,127.5 255,0"
></
polygon
></
svg
>
)
const
Tooltip
:
FC
<
TooltipProps
>
=
({
position
=
'top'
,
triggerMethod
=
'hover'
,
popupContent
,
children
,
})
=>
{
const
[
open
,
setOpen
]
=
useState
(
false
)
return
(
<
PortalToFollowElem
open=
{
open
}
onOpenChange=
{
setOpen
}
placement=
{
position
}
offset=
{
10
}
>
<
PortalToFollowElemTrigger
onClick=
{
()
=>
triggerMethod
===
'click'
&&
setOpen
(
v
=>
!
v
)
}
onMouseEnter=
{
()
=>
triggerMethod
===
'hover'
&&
setOpen
(
true
)
}
onMouseLeave=
{
()
=>
triggerMethod
===
'hover'
&&
setOpen
(
false
)
}
>
{
children
}
</
PortalToFollowElemTrigger
>
<
PortalToFollowElemContent
className=
"z-[999]"
>
<
div
className=
'relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg'
>
{
popupContent
}
{
arrow
}
</
div
>
</
PortalToFollowElemContent
>
</
PortalToFollowElem
>
)
}
export
default
React
.
memo
(
Tooltip
)
app/components/chat/index.tsx
View file @
5dc3658e
...
@@ -4,14 +4,19 @@ import React, { useEffect, useRef } from 'react'
...
@@ -4,14 +4,19 @@ import React, { useEffect, useRef } from 'react'
import
cn
from
'classnames'
import
cn
from
'classnames'
import
{
HandThumbDownIcon
,
HandThumbUpIcon
}
from
'@heroicons/react/24/outline'
import
{
HandThumbDownIcon
,
HandThumbUpIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
Textarea
from
'rc-textarea'
import
s
from
'./style.module.css'
import
s
from
'./style.module.css'
import
LoadingAnim
from
'./loading-anim'
import
LoadingAnim
from
'./loading-anim'
import
{
randomString
}
from
'@/utils/string'
import
{
randomString
}
from
'@/utils/string'
import
type
{
Feedbacktype
,
MessageRating
}
from
'@/types/app'
import
type
{
Feedbacktype
,
MessageRating
,
VisionFile
,
VisionSettings
}
from
'@/types/app'
import
{
TransferMethod
}
from
'@/types/app'
import
Tooltip
from
'@/app/components/base/tooltip'
import
Tooltip
from
'@/app/components/base/tooltip'
import
Toast
from
'@/app/components/base/toast'
import
Toast
from
'@/app/components/base/toast'
import
AutoHeightTextarea
from
'@/app/components/base/auto-height-textarea'
import
{
Markdown
}
from
'@/app/components/base/markdown'
import
{
Markdown
}
from
'@/app/components/base/markdown'
import
ChatImageUploader
from
'@/app/components/base/image-uploader/chat-image-uploader'
import
ImageList
from
'@/app/components/base/image-uploader/image-list'
import
{
useImageFiles
}
from
'@/app/components/base/image-uploader/hooks'
import
ImageGallery
from
'@/app/components/base/image-gallery'
export
type
FeedbackFunc
=
(
messageId
:
string
,
feedback
:
Feedbacktype
)
=>
Promise
<
any
>
export
type
FeedbackFunc
=
(
messageId
:
string
,
feedback
:
Feedbacktype
)
=>
Promise
<
any
>
...
@@ -27,11 +32,11 @@ export type IChatProps = {
...
@@ -27,11 +32,11 @@ export type IChatProps = {
isHideSendInput
?:
boolean
isHideSendInput
?:
boolean
onFeedback
?:
FeedbackFunc
onFeedback
?:
FeedbackFunc
checkCanSend
?:
()
=>
boolean
checkCanSend
?:
()
=>
boolean
onSend
?:
(
message
:
string
)
=>
void
onSend
?:
(
message
:
string
,
files
:
VisionFile
[]
)
=>
void
useCurrentUserAvatar
?:
boolean
useCurrentUserAvatar
?:
boolean
isResponsing
?:
boolean
isResponsing
?:
boolean
controlClearQuery
?:
number
controlClearQuery
?:
number
controlFocus
?:
number
visionConfig
?:
VisionSettings
}
}
export
type
IChatItem
=
{
export
type
IChatItem
=
{
...
@@ -52,6 +57,7 @@ export type IChatItem = {
...
@@ -52,6 +57,7 @@ export type IChatItem = {
isIntroduction
?:
boolean
isIntroduction
?:
boolean
useCurrentUserAvatar
?:
boolean
useCurrentUserAvatar
?:
boolean
isOpeningStatement
?:
boolean
isOpeningStatement
?:
boolean
message_files
?:
VisionFile
[]
}
}
const
OperationBtn
=
({
innerContent
,
onClick
,
className
}:
{
innerContent
:
React
.
ReactNode
;
onClick
?:
()
=>
void
;
className
?:
string
})
=>
(
const
OperationBtn
=
({
innerContent
,
onClick
,
className
}:
{
innerContent
:
React
.
ReactNode
;
onClick
?:
()
=>
void
;
className
?:
string
})
=>
(
...
@@ -205,9 +211,11 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
...
@@ -205,9 +211,11 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
)
)
}
}
type
IQuestionProps
=
Pick
<
IChatItem
,
'id'
|
'content'
|
'useCurrentUserAvatar'
>
type
IQuestionProps
=
Pick
<
IChatItem
,
'id'
|
'content'
|
'useCurrentUserAvatar'
>
&
{
imgSrcs
?:
string
[]
}
const
Question
:
FC
<
IQuestionProps
>
=
({
id
,
content
,
useCurrentUserAvatar
})
=>
{
const
Question
:
FC
<
IQuestionProps
>
=
({
id
,
content
,
useCurrentUserAvatar
,
imgSrcs
})
=>
{
const
userName
=
''
const
userName
=
''
return
(
return
(
<
div
className=
'flex items-start justify-end'
key=
{
id
}
>
<
div
className=
'flex items-start justify-end'
key=
{
id
}
>
...
@@ -216,6 +224,9 @@ const Question: FC<IQuestionProps> = ({ id, content, useCurrentUserAvatar }) =>
...
@@ -216,6 +224,9 @@ const Question: FC<IQuestionProps> = ({ id, content, useCurrentUserAvatar }) =>
<
div
<
div
className=
{
'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'
}
className=
{
'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'
}
>
>
{
imgSrcs
&&
imgSrcs
.
length
>
0
&&
(
<
ImageGallery
srcs=
{
imgSrcs
}
/>
)
}
<
Markdown
content=
{
content
}
/>
<
Markdown
content=
{
content
}
/>
</
div
>
</
div
>
</
div
>
</
div
>
...
@@ -243,7 +254,7 @@ const Chat: FC<IChatProps> = ({
...
@@ -243,7 +254,7 @@ const Chat: FC<IChatProps> = ({
useCurrentUserAvatar
,
useCurrentUserAvatar
,
isResponsing
,
isResponsing
,
controlClearQuery
,
controlClearQuery
,
controlFocus
,
visionConfig
,
})
=>
{
})
=>
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
{
notify
}
=
Toast
const
{
notify
}
=
Toast
...
@@ -271,14 +282,32 @@ const Chat: FC<IChatProps> = ({
...
@@ -271,14 +282,32 @@ const Chat: FC<IChatProps> = ({
if
(
controlClearQuery
)
if
(
controlClearQuery
)
setQuery
(
''
)
setQuery
(
''
)
},
[
controlClearQuery
])
},
[
controlClearQuery
])
const
{
files
,
onUpload
,
onRemove
,
onReUpload
,
onImageLinkLoadError
,
onImageLinkLoadSuccess
,
onClear
,
}
=
useImageFiles
()
const
handleSend
=
()
=>
{
const
handleSend
=
()
=>
{
if
(
!
valid
()
||
(
checkCanSend
&&
!
checkCanSend
()))
if
(
!
valid
()
||
(
checkCanSend
&&
!
checkCanSend
()))
return
return
onSend
(
query
)
onSend
(
query
,
files
.
filter
(
file
=>
file
.
progress
!==
-
1
).
map
(
fileItem
=>
({
type
:
'image'
,
transfer_method
:
fileItem
.
type
,
url
:
fileItem
.
url
,
upload_file_id
:
fileItem
.
fileId
,
})))
if
(
!
files
.
find
(
item
=>
item
.
type
===
TransferMethod
.
local_file
&&
!
item
.
fileId
))
{
if
(
files
.
length
)
onClear
()
if
(
!
isResponsing
)
if
(
!
isResponsing
)
setQuery
(
''
)
setQuery
(
''
)
}
}
}
const
handleKeyUp
=
(
e
:
any
)
=>
{
const
handleKeyUp
=
(
e
:
any
)
=>
{
if
(
e
.
code
===
'Enter'
)
{
if
(
e
.
code
===
'Enter'
)
{
...
@@ -289,7 +318,7 @@ const Chat: FC<IChatProps> = ({
...
@@ -289,7 +318,7 @@ const Chat: FC<IChatProps> = ({
}
}
}
}
const
han
e
leKeyDown
=
(
e
:
any
)
=>
{
const
han
d
leKeyDown
=
(
e
:
any
)
=>
{
isUseInputMethod
.
current
=
e
.
nativeEvent
.
isComposing
isUseInputMethod
.
current
=
e
.
nativeEvent
.
isComposing
if
(
e
.
code
===
'Enter'
&&
!
e
.
shiftKey
)
{
if
(
e
.
code
===
'Enter'
&&
!
e
.
shiftKey
)
{
setQuery
(
query
.
replace
(
/
\n
$/
,
''
))
setQuery
(
query
.
replace
(
/
\n
$/
,
''
))
...
@@ -312,24 +341,56 @@ const Chat: FC<IChatProps> = ({
...
@@ -312,24 +341,56 @@ const Chat: FC<IChatProps> = ({
isResponsing=
{
isResponsing
&&
isLast
}
isResponsing=
{
isResponsing
&&
isLast
}
/>
/>
}
}
return
<
Question
key=
{
item
.
id
}
id=
{
item
.
id
}
content=
{
item
.
content
}
useCurrentUserAvatar=
{
useCurrentUserAvatar
}
/>
return
(
<
Question
key=
{
item
.
id
}
id=
{
item
.
id
}
content=
{
item
.
content
}
useCurrentUserAvatar=
{
useCurrentUserAvatar
}
imgSrcs=
{
(
item
.
message_files
&&
item
.
message_files
?.
length
>
0
)
?
item
.
message_files
.
map
(
item
=>
item
.
url
)
:
[]
}
/>
)
})
}
})
}
</
div
>
</
div
>
{
{
!
isHideSendInput
&&
(
!
isHideSendInput
&&
(
<
div
className=
{
cn
(
!
feedbackDisabled
&&
'!left-3.5 !right-3.5'
,
'absolute z-10 bottom-0 left-0 right-0'
)
}
>
<
div
className=
{
cn
(
!
feedbackDisabled
&&
'!left-3.5 !right-3.5'
,
'absolute z-10 bottom-0 left-0 right-0'
)
}
>
<
div
className=
"positive"
>
<
div
className=
'p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto'
>
<
AutoHeightTextarea
{
visionConfig
?.
enabled
&&
(
<>
<
div
className=
'absolute bottom-2 left-2 flex items-center'
>
<
ChatImageUploader
settings=
{
visionConfig
}
onUpload=
{
onUpload
}
disabled=
{
files
.
length
>=
visionConfig
.
number_limits
}
/>
<
div
className=
'mx-1 w-[1px] h-4 bg-black/5'
/>
</
div
>
<
div
className=
'pl-[52px]'
>
<
ImageList
list=
{
files
}
onRemove=
{
onRemove
}
onReUpload=
{
onReUpload
}
onImageLinkLoadSuccess=
{
onImageLinkLoadSuccess
}
onImageLinkLoadError=
{
onImageLinkLoadError
}
/>
</
div
>
</>
)
}
<
Textarea
className=
{
`
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
${visionConfig?.enabled && 'pl-12'}
`
}
value=
{
query
}
value=
{
query
}
onChange=
{
handleContentChange
}
onChange=
{
handleContentChange
}
onKeyUp=
{
handleKeyUp
}
onKeyUp=
{
handleKeyUp
}
onKeyDown=
{
haneleKeyDown
}
onKeyDown=
{
handleKeyDown
}
minHeight=
{
48
}
autoSize
autoFocus
controlFocus=
{
controlFocus
}
className=
{
`${cn(s.textArea)} resize-none block w-full pl-3 bg-gray-50 border border-gray-200 rounded-md focus:outline-none sm:text-sm text-gray-700`
}
/>
/>
<
div
className=
"absolute
top-0 right-2 flex items-center h-[48px]
"
>
<
div
className=
"absolute
bottom-2 right-2 flex items-center h-8
"
>
<
div
className=
{
`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`
}
>
{
query
.
trim
().
length
}
</
div
>
<
div
className=
{
`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`
}
>
{
query
.
trim
().
length
}
</
div
>
<
Tooltip
<
Tooltip
selector=
'send-tip'
selector=
'send-tip'
...
...
app/components/index.tsx
View file @
5dc3658e
...
@@ -11,7 +11,8 @@ import Sidebar from '@/app/components/sidebar'
...
@@ -11,7 +11,8 @@ import Sidebar from '@/app/components/sidebar'
import
ConfigSence
from
'@/app/components/config-scence'
import
ConfigSence
from
'@/app/components/config-scence'
import
Header
from
'@/app/components/header'
import
Header
from
'@/app/components/header'
import
{
fetchAppParams
,
fetchChatList
,
fetchConversations
,
sendChatMessage
,
updateFeedback
}
from
'@/service'
import
{
fetchAppParams
,
fetchChatList
,
fetchConversations
,
sendChatMessage
,
updateFeedback
}
from
'@/service'
import
type
{
ConversationItem
,
Feedbacktype
,
IChatItem
,
PromptConfig
}
from
'@/types/app'
import
type
{
ConversationItem
,
Feedbacktype
,
IChatItem
,
PromptConfig
,
VisionFile
,
VisionSettings
}
from
'@/types/app'
import
{
TransferMethod
}
from
'@/types/app'
import
Chat
from
'@/app/components/chat'
import
Chat
from
'@/app/components/chat'
import
{
setLocaleOnClient
}
from
'@/i18n/client'
import
{
setLocaleOnClient
}
from
'@/i18n/client'
import
useBreakpoints
,
{
MediaType
}
from
'@/hooks/use-breakpoints'
import
useBreakpoints
,
{
MediaType
}
from
'@/hooks/use-breakpoints'
...
@@ -35,6 +36,7 @@ const Main: FC = () => {
...
@@ -35,6 +36,7 @@ const Main: FC = () => {
const
[
inited
,
setInited
]
=
useState
<
boolean
>
(
false
)
const
[
inited
,
setInited
]
=
useState
<
boolean
>
(
false
)
// in mobile, show sidebar by click button
// in mobile, show sidebar by click button
const
[
isShowSidebar
,
{
setTrue
:
showSidebar
,
setFalse
:
hideSidebar
}]
=
useBoolean
(
false
)
const
[
isShowSidebar
,
{
setTrue
:
showSidebar
,
setFalse
:
hideSidebar
}]
=
useBoolean
(
false
)
const
[
visionConfig
,
setVisionConfig
]
=
useState
<
VisionSettings
|
undefined
>
(
undefined
)
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
APP_INFO
?.
title
)
if
(
APP_INFO
?.
title
)
...
@@ -113,6 +115,7 @@ const Main: FC = () => {
...
@@ -113,6 +115,7 @@ const Main: FC = () => {
id
:
`question-
${
item
.
id
}
`
,
id
:
`question-
${
item
.
id
}
`
,
content
:
item
.
query
,
content
:
item
.
query
,
isAnswer
:
false
,
isAnswer
:
false
,
message_files
:
item
.
message_files
,
})
})
newChatList
.
push
({
newChatList
.
push
({
id
:
item
.
id
,
id
:
item
.
id
,
...
@@ -127,8 +130,6 @@ const Main: FC = () => {
...
@@ -127,8 +130,6 @@ const Main: FC = () => {
if
(
isNewConversation
&&
isChatStarted
)
if
(
isNewConversation
&&
isChatStarted
)
setChatList
(
generateNewChatListWithOpenstatement
())
setChatList
(
generateNewChatListWithOpenstatement
())
setControlFocus
(
Date
.
now
())
}
}
useEffect
(
handleConversationSwitch
,
[
currConversationId
,
inited
])
useEffect
(
handleConversationSwitch
,
[
currConversationId
,
inited
])
...
@@ -208,7 +209,7 @@ const Main: FC = () => {
...
@@ -208,7 +209,7 @@ const Main: FC = () => {
const
isNotNewConversation
=
conversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
const
isNotNewConversation
=
conversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
// fetch new conversation info
// fetch new conversation info
const
{
user_input_form
,
opening_statement
:
introduction
}:
any
=
appParams
const
{
user_input_form
,
opening_statement
:
introduction
,
file_upload
,
system_parameters
}:
any
=
appParams
setLocaleOnClient
(
APP_INFO
.
default_language
,
true
)
setLocaleOnClient
(
APP_INFO
.
default_language
,
true
)
setNewConversationInfo
({
setNewConversationInfo
({
name
:
t
(
'app.chat.newChatDefaultName'
),
name
:
t
(
'app.chat.newChatDefaultName'
),
...
@@ -219,7 +220,10 @@ const Main: FC = () => {
...
@@ -219,7 +220,10 @@ const Main: FC = () => {
prompt_template
:
promptTemplate
,
prompt_template
:
promptTemplate
,
prompt_variables
,
prompt_variables
,
}
as
PromptConfig
)
}
as
PromptConfig
)
setVisionConfig
({
...
file_upload
?.
image
,
image_file_size_limit
:
system_parameters
?.
system_parameters
||
0
,
})
setConversationList
(
conversations
as
ConversationItem
[])
setConversationList
(
conversations
as
ConversationItem
[])
if
(
isNotNewConversation
)
if
(
isNotNewConversation
)
...
@@ -263,24 +267,36 @@ const Main: FC = () => {
...
@@ -263,24 +267,36 @@ const Main: FC = () => {
return
true
return
true
}
}
const
[
controlFocus
,
setControlFocus
]
=
useState
(
0
)
const
handleSend
=
async
(
message
:
string
,
files
?:
VisionFile
[])
=>
{
const
handleSend
=
async
(
message
:
string
)
=>
{
if
(
isResponsing
)
{
if
(
isResponsing
)
{
notify
({
type
:
'info'
,
message
:
t
(
'app.errorMessage.waitForResponse'
)
})
notify
({
type
:
'info'
,
message
:
t
(
'app.errorMessage.waitForResponse'
)
})
return
return
}
}
const
data
=
{
const
data
:
Record
<
string
,
any
>
=
{
inputs
:
currInputs
,
inputs
:
currInputs
,
query
:
message
,
query
:
message
,
conversation_id
:
isNewConversation
?
null
:
currConversationId
,
conversation_id
:
isNewConversation
?
null
:
currConversationId
,
}
}
if
(
visionConfig
?.
enabled
&&
files
&&
files
?.
length
>
0
)
{
data
.
files
=
files
.
map
((
item
)
=>
{
if
(
item
.
transfer_method
===
TransferMethod
.
local_file
)
{
return
{
...
item
,
url
:
''
,
}
}
return
item
})
}
// qustion
// qustion
const
questionId
=
`question-
${
Date
.
now
()}
`
const
questionId
=
`question-
${
Date
.
now
()}
`
const
questionItem
=
{
const
questionItem
=
{
id
:
questionId
,
id
:
questionId
,
content
:
message
,
content
:
message
,
isAnswer
:
false
,
isAnswer
:
false
,
message_files
:
files
,
}
}
const
placeholderAnswerId
=
`answer-placeholder-
${
Date
.
now
()}
`
const
placeholderAnswerId
=
`answer-placeholder-
${
Date
.
now
()}
`
...
@@ -423,7 +439,7 @@ const Main: FC = () => {
...
@@ -423,7 +439,7 @@ const Main: FC = () => {
onFeedback=
{
handleFeedback
}
onFeedback=
{
handleFeedback
}
isResponsing=
{
isResponsing
}
isResponsing=
{
isResponsing
}
checkCanSend=
{
checkCanSend
}
checkCanSend=
{
checkCanSend
}
controlFocus=
{
controlFocus
}
visionConfig=
{
visionConfig
}
/>
/>
</
div
>
</
div
>
</
div
>)
</
div
>)
...
...
i18n/lang/common.en.ts
View file @
5dc3658e
...
@@ -16,6 +16,17 @@ const translation = {
...
@@ -16,6 +16,17 @@ const translation = {
lineBreak
:
'Line break'
,
lineBreak
:
'Line break'
,
like
:
'like'
,
like
:
'like'
,
dislike
:
'dislike'
,
dislike
:
'dislike'
,
ok
:
'OK'
,
},
imageUploader
:
{
uploadFromComputer
:
'Upload from Computer'
,
uploadFromComputerReadError
:
'Image reading failed, please try again.'
,
uploadFromComputerUploadError
:
'Image upload failed, please upload again.'
,
uploadFromComputerLimit
:
'Upload images cannot exceed {{size}} MB'
,
pasteImageLink
:
'Paste image link'
,
pasteImageLinkInputPlaceholder
:
'Paste image link here'
,
pasteImageLinkInvalid
:
'Invalid image link'
,
imageUpload
:
'Image Upload'
,
},
},
}
}
...
...
i18n/lang/common.zh.ts
View file @
5dc3658e
...
@@ -16,6 +16,17 @@ const translation = {
...
@@ -16,6 +16,17 @@ const translation = {
lineBreak
:
'换行'
,
lineBreak
:
'换行'
,
like
:
'赞同'
,
like
:
'赞同'
,
dislike
:
'反对'
,
dislike
:
'反对'
,
ok
:
'好的'
,
},
imageUploader
:
{
uploadFromComputer
:
'从本地上传'
,
uploadFromComputerReadError
:
'图片读取失败,请重新选择。'
,
uploadFromComputerUploadError
:
'图片上传失败,请重新上传。'
,
uploadFromComputerLimit
:
'上传图片不能超过 {{size}} MB'
,
pasteImageLink
:
'粘贴图片链接'
,
pasteImageLinkInputPlaceholder
:
'将图像链接粘贴到此处'
,
pasteImageLinkInvalid
:
'图片链接无效'
,
imageUpload
:
'图片上传'
,
},
},
}
}
...
...
package.json
View file @
5dc3658e
...
@@ -12,6 +12,7 @@
...
@@ -12,6 +12,7 @@
"prepare"
:
"husky install ./.husky"
"prepare"
:
"husky install ./.husky"
},
},
"dependencies"
:
{
"dependencies"
:
{
"
@floating-ui/react
"
:
"
^0.26.2
"
,
"
@formatjs/intl-localematcher
"
:
"
^0.2.32
"
,
"
@formatjs/intl-localematcher
"
:
"
^0.2.32
"
,
"
@headlessui/react
"
:
"
^1.7.13
"
,
"
@headlessui/react
"
:
"
^1.7.13
"
,
"
@heroicons/react
"
:
"
^2.0.16
"
,
"
@heroicons/react
"
:
"
^2.0.16
"
,
...
@@ -26,7 +27,7 @@
...
@@ -26,7 +27,7 @@
"
axios
"
:
"
^1.3.5
"
,
"
axios
"
:
"
^1.3.5
"
,
"
classnames
"
:
"
^2.3.2
"
,
"
classnames
"
:
"
^2.3.2
"
,
"
copy-to-clipboard
"
:
"
^3.3.3
"
,
"
copy-to-clipboard
"
:
"
^3.3.3
"
,
"
dify-client
"
:
"
2.0
.0
"
,
"
dify-client
"
:
"
^2.1
.0
"
,
"
eslint
"
:
"
8.36.0
"
,
"
eslint
"
:
"
8.36.0
"
,
"
eslint-config-next
"
:
"
13.4.0
"
,
"
eslint-config-next
"
:
"
13.4.0
"
,
"
eventsource-parser
"
:
"
^1.0.0
"
,
"
eventsource-parser
"
:
"
^1.0.0
"
,
...
@@ -38,6 +39,7 @@
...
@@ -38,6 +39,7 @@
"
katex
"
:
"
^0.16.7
"
,
"
katex
"
:
"
^0.16.7
"
,
"
negotiator
"
:
"
^0.6.3
"
,
"
negotiator
"
:
"
^0.6.3
"
,
"
next
"
:
"
13.4.0
"
,
"
next
"
:
"
13.4.0
"
,
"
rc-textarea
"
:
"
^1.5.3
"
,
"
react
"
:
"
18.2.0
"
,
"
react
"
:
"
18.2.0
"
,
"
react-dom
"
:
"
18.2.0
"
,
"
react-dom
"
:
"
18.2.0
"
,
"
react-error-boundary
"
:
"
^4.0.2
"
,
"
react-error-boundary
"
:
"
^4.0.2
"
,
...
...
service/base.ts
View file @
5dc3658e
...
@@ -66,7 +66,8 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
...
@@ -66,7 +66,8 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
return
return
try
{
try
{
bufferObj
=
JSON
.
parse
(
message
.
substring
(
6
))
// remove data: and parse as json
bufferObj
=
JSON
.
parse
(
message
.
substring
(
6
))
// remove data: and parse as json
}
catch
(
e
)
{
}
catch
(
e
)
{
// mute handle message cut off
// mute handle message cut off
onData
(
''
,
isFirstMessage
,
{
onData
(
''
,
isFirstMessage
,
{
conversationId
:
bufferObj
?.
conversation_id
,
conversationId
:
bufferObj
?.
conversation_id
,
...
@@ -181,6 +182,38 @@ const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: I
...
@@ -181,6 +182,38 @@ const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: I
])
])
}
}
export
const
upload
=
(
fetchOptions
:
any
):
Promise
<
any
>
=>
{
const
urlPrefix
=
API_PREFIX
const
urlWithPrefix
=
`
${
urlPrefix
}
/file-upload`
const
defaultOptions
=
{
method
:
'POST'
,
url
:
`
${
urlWithPrefix
}
`
,
data
:
{},
}
const
options
=
{
...
defaultOptions
,
...
fetchOptions
,
}
return
new
Promise
((
resolve
,
reject
)
=>
{
const
xhr
=
options
.
xhr
xhr
.
open
(
options
.
method
,
options
.
url
)
for
(
const
key
in
options
.
headers
)
xhr
.
setRequestHeader
(
key
,
options
.
headers
[
key
])
xhr
.
withCredentials
=
true
xhr
.
onreadystatechange
=
function
()
{
if
(
xhr
.
readyState
===
4
)
{
if
(
xhr
.
status
===
200
)
resolve
({
id
:
xhr
.
response
})
else
reject
(
xhr
)
}
}
xhr
.
upload
.
onprogress
=
options
.
onprogress
xhr
.
send
(
options
.
data
)
})
}
export
const
ssePost
=
(
url
:
string
,
fetchOptions
:
any
,
{
onData
,
onCompleted
,
onError
}:
IOtherOptions
)
=>
{
export
const
ssePost
=
(
url
:
string
,
fetchOptions
:
any
,
{
onData
,
onCompleted
,
onError
}:
IOtherOptions
)
=>
{
const
options
=
Object
.
assign
({},
baseOptions
,
{
const
options
=
Object
.
assign
({},
baseOptions
,
{
method
:
'POST'
,
method
:
'POST'
,
...
...
types/app.ts
View file @
5dc3658e
...
@@ -77,6 +77,7 @@ export type IChatItem = {
...
@@ -77,6 +77,7 @@ export type IChatItem = {
isIntroduction
?:
boolean
isIntroduction
?:
boolean
useCurrentUserAvatar
?:
boolean
useCurrentUserAvatar
?:
boolean
isOpeningStatement
?:
boolean
isOpeningStatement
?:
boolean
message_files
?:
VisionFile
[]
}
}
export
type
ResponseHolder
=
{}
export
type
ResponseHolder
=
{}
...
@@ -95,3 +96,41 @@ export type AppInfo = {
...
@@ -95,3 +96,41 @@ export type AppInfo = {
copyright
?:
string
copyright
?:
string
privacy_policy
?:
string
privacy_policy
?:
string
}
}
export
enum
Resolution
{
low
=
'low'
,
high
=
'high'
,
}
export
enum
TransferMethod
{
all
=
'all'
,
local_file
=
'local_file'
,
remote_url
=
'remote_url'
,
}
export
type
VisionSettings
=
{
enabled
:
boolean
number_limits
:
number
detail
:
Resolution
transfer_methods
:
TransferMethod
[]
image_file_size_limit
?:
number
|
string
}
export
type
ImageFile
=
{
type
:
TransferMethod
_id
:
string
fileId
:
string
file
?:
File
progress
:
number
url
:
string
base64Url
?:
string
deleted
?:
boolean
}
export
type
VisionFile
=
{
id
?:
string
type
:
string
transfer_method
:
TransferMethod
url
:
string
upload_file_id
:
string
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment