Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
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
dify
Commits
a0b3891c
Commit
a0b3891c
authored
Jul 01, 2023
by
Gillian97
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: chatbot init
parent
345399a3
Changes
23
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
1731 additions
and
24 deletions
+1731
-24
page.tsx
web/app/(shareLayout)/chatbot/[token]/page.tsx
+13
-0
index.tsx
web/app/components/app/chat/index.tsx
+2
-2
index.tsx
web/app/components/app/overview/embedded/index.tsx
+16
-5
index.tsx
web/app/components/share/chatbot/config-scence/index.tsx
+13
-0
use-conversation.ts
web/app/components/share/chatbot/hooks/use-conversation.ts
+70
-0
index.tsx
web/app/components/share/chatbot/index.tsx
+636
-0
index.tsx
web/app/components/share/chatbot/sidebar/app-info/index.tsx
+28
-0
card.module.css
web/app/components/share/chatbot/sidebar/card.module.css
+3
-0
card.tsx
web/app/components/share/chatbot/sidebar/card.tsx
+19
-0
index.tsx
web/app/components/share/chatbot/sidebar/index.tsx
+151
-0
index.tsx
web/app/components/share/chatbot/sidebar/list/index.tsx
+115
-0
style.module.css
...pp/components/share/chatbot/sidebar/list/style.module.css
+7
-0
style.module.css
web/app/components/share/chatbot/style.module.css
+3
-0
index.tsx
web/app/components/share/chatbot/value-panel/index.tsx
+79
-0
style.module.css
...app/components/share/chatbot/value-panel/style.module.css
+3
-0
logo.png
web/app/components/share/chatbot/welcome/icons/logo.png
+0
-0
index.tsx
web/app/components/share/chatbot/welcome/index.tsx
+356
-0
massive-component.tsx
...pp/components/share/chatbot/welcome/massive-component.tsx
+74
-0
style.module.css
web/app/components/share/chatbot/welcome/style.module.css
+29
-0
header.tsx
web/app/components/share/header.tsx
+23
-17
app-overview.en.ts
web/i18n/lang/app-overview.en.ts
+2
-0
app-overview.zh.ts
web/i18n/lang/app-overview.zh.ts
+2
-0
embed.js
web/public/embed.js
+87
-0
No files found.
web/app/(shareLayout)/chatbot/[token]/page.tsx
0 → 100644
View file @
a0b3891c
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
type
{
IMainProps
}
from
'@/app/components/share/chat'
import
Main
from
'@/app/components/share/chatbot'
const
Chatbot
:
FC
<
IMainProps
>
=
()
=>
{
return
(
<
Main
/>
)
}
export
default
React
.
memo
(
Chatbot
)
web/app/components/app/chat/index.tsx
View file @
a0b3891c
...
...
@@ -466,7 +466,7 @@ const Chat: FC<IChatProps> = ({
}
}
const
han
e
leKeyDown
=
(
e
:
any
)
=>
{
const
han
d
leKeyDown
=
(
e
:
any
)
=>
{
isUseInputMethod
.
current
=
e
.
nativeEvent
.
isComposing
if
(
e
.
code
===
'Enter'
&&
!
e
.
shiftKey
)
{
setQuery
(
query
.
replace
(
/
\n
$/
,
''
))
...
...
@@ -557,7 +557,7 @@ const Chat: FC<IChatProps> = ({
value=
{
query
}
onChange=
{
handleContentChange
}
onKeyUp=
{
handleKeyUp
}
onKeyDown=
{
han
e
leKeyDown
}
onKeyDown=
{
han
d
leKeyDown
}
minHeight=
{
48
}
autoFocus
controlFocus=
{
controlFocus
}
...
...
web/app/components/app/overview/embedded/index.tsx
View file @
a0b3891c
...
...
@@ -5,6 +5,9 @@ import style from './style.module.css'
import
Modal
from
'@/app/components/base/modal'
import
useCopyToClipboard
from
'@/hooks/use-copy-to-clipboard'
import
copyStyle
from
'@/app/components/app/chat/copy-btn/style.module.css'
import
Tooltip
from
'@/app/components/base/tooltip'
const
isDevelopment
=
process
.
env
.
NODE_ENV
===
'development'
type
Props
=
{
isShow
:
boolean
...
...
@@ -17,14 +20,17 @@ const OPTION_MAP = {
iframe
:
{
getContent
:
(
url
:
string
,
token
:
string
)
=>
`<iframe
src="
${
url
}
/c
ompletion
/
${
token
}
"
src="
${
url
}
/c
hatbot
/
${
token
}
"
style="width: 100%; height: 100%; min-height: 700px"
frameborder="0" >
</iframe>`
,
},
scripts
:
{
getContent
:
(
url
:
string
,
token
:
string
)
=>
`<script
`<script>
window.difyChatbotConfig = { token:
${
token
}${
isDevelopment
?
', isDev: true'
:
''
}
}
</script>
<script
src="
${
url
}
/embed.min.js"
id="
${
token
}
"
defer>
...
...
@@ -78,9 +84,14 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
{
t
(
`${prefixEmbedded}.${option}`
)
}
</
div
>
<
div
className=
"p-2 rounded-lg justify-center items-center gap-1 flex"
>
<
div
className=
"w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg"
>
<
div
onClick=
{
onClickCopy
}
className=
{
`w-full h-full ${copyStyle.copyIcon} ${isCopied[option] ? copyStyle.copied : ''}`
}
></
div
>
</
div
>
<
Tooltip
selector=
{
'code-copy-feedback'
}
content=
{
(
isCopied
[
option
]
?
t
(
`${prefixEmbedded}.copied`
)
:
t
(
`${prefixEmbedded}.copy`
))
||
''
}
>
<
div
className=
"w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg"
>
<
div
onClick=
{
onClickCopy
}
className=
{
`w-full h-full ${copyStyle.copyIcon} ${isCopied[option] ? copyStyle.copied : ''}`
}
></
div
>
</
div
>
</
Tooltip
>
</
div
>
</
div
>
<
div
className=
"self-stretch p-3 justify-start items-start gap-2 inline-flex"
>
...
...
web/app/components/share/chatbot/config-scence/index.tsx
0 → 100644
View file @
a0b3891c
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
type
{
IWelcomeProps
}
from
'../welcome'
import
Welcome
from
'../welcome'
const
ConfigScene
:
FC
<
IWelcomeProps
>
=
(
props
)
=>
{
return
(
<
div
className=
'mb-5 antialiased font-sans shrink-0'
>
<
Welcome
{
...
props
}
/>
</
div
>
)
}
export
default
React
.
memo
(
ConfigScene
)
web/app/components/share/chatbot/hooks/use-conversation.ts
0 → 100644
View file @
a0b3891c
import
{
useState
}
from
'react'
import
produce
from
'immer'
import
type
{
ConversationItem
}
from
'@/models/share'
const
storageConversationIdKey
=
'conversationIdInfo'
type
ConversationInfoType
=
Omit
<
ConversationItem
,
'inputs'
|
'id'
>
function
useConversation
()
{
const
[
conversationList
,
setConversationList
]
=
useState
<
ConversationItem
[]
>
([])
const
[
pinnedConversationList
,
setPinnedConversationList
]
=
useState
<
ConversationItem
[]
>
([])
const
[
currConversationId
,
doSetCurrConversationId
]
=
useState
<
string
>
(
'-1'
)
// when set conversation id, we do not have set appId
const
setCurrConversationId
=
(
id
:
string
,
appId
:
string
,
isSetToLocalStroge
=
true
,
newConversationName
=
''
)
=>
{
doSetCurrConversationId
(
id
)
if
(
isSetToLocalStroge
&&
id
!==
'-1'
)
{
// conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2}
const
conversationIdInfo
=
globalThis
.
localStorage
?.
getItem
(
storageConversationIdKey
)
?
JSON
.
parse
(
globalThis
.
localStorage
?.
getItem
(
storageConversationIdKey
)
||
''
)
:
{}
conversationIdInfo
[
appId
]
=
id
globalThis
.
localStorage
?.
setItem
(
storageConversationIdKey
,
JSON
.
stringify
(
conversationIdInfo
))
}
}
const
getConversationIdFromStorage
=
(
appId
:
string
)
=>
{
const
conversationIdInfo
=
globalThis
.
localStorage
?.
getItem
(
storageConversationIdKey
)
?
JSON
.
parse
(
globalThis
.
localStorage
?.
getItem
(
storageConversationIdKey
)
||
''
)
:
{}
const
id
=
conversationIdInfo
[
appId
]
return
id
}
const
isNewConversation
=
currConversationId
===
'-1'
// input can be updated by user
const
[
newConversationInputs
,
setNewConversationInputs
]
=
useState
<
Record
<
string
,
any
>
|
null
>
(
null
)
const
resetNewConversationInputs
=
()
=>
{
if
(
!
newConversationInputs
)
return
setNewConversationInputs
(
produce
(
newConversationInputs
,
(
draft
)
=>
{
Object
.
keys
(
draft
).
forEach
((
key
)
=>
{
draft
[
key
]
=
''
})
}))
}
const
[
existConversationInputs
,
setExistConversationInputs
]
=
useState
<
Record
<
string
,
any
>
|
null
>
(
null
)
const
currInputs
=
isNewConversation
?
newConversationInputs
:
existConversationInputs
const
setCurrInputs
=
isNewConversation
?
setNewConversationInputs
:
setExistConversationInputs
// info is muted
const
[
newConversationInfo
,
setNewConversationInfo
]
=
useState
<
ConversationInfoType
|
null
>
(
null
)
const
[
existConversationInfo
,
setExistConversationInfo
]
=
useState
<
ConversationInfoType
|
null
>
(
null
)
const
currConversationInfo
=
isNewConversation
?
newConversationInfo
:
existConversationInfo
return
{
conversationList
,
setConversationList
,
pinnedConversationList
,
setPinnedConversationList
,
currConversationId
,
setCurrConversationId
,
getConversationIdFromStorage
,
isNewConversation
,
currInputs
,
newConversationInputs
,
existConversationInputs
,
resetNewConversationInputs
,
setCurrInputs
,
currConversationInfo
,
setNewConversationInfo
,
setExistConversationInfo
,
}
}
export
default
useConversation
web/app/components/share/chatbot/index.tsx
0 → 100644
View file @
a0b3891c
/* eslint-disable @typescript-eslint/no-use-before-define */
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useEffect
,
useRef
,
useState
}
from
'react'
import
cn
from
'classnames'
import
{
useTranslation
}
from
'react-i18next'
import
{
useContext
}
from
'use-context-selector'
import
produce
from
'immer'
import
{
useBoolean
,
useGetState
}
from
'ahooks'
import
AppUnavailable
from
'../../base/app-unavailable'
import
useConversation
from
'./hooks/use-conversation'
import
s
from
'./style.module.css'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
Sidebar
from
'@/app/components/share/chatbot/sidebar'
import
ConfigScene
from
'@/app/components/share/chatbot/config-scence'
import
Header
from
'@/app/components/share/header'
import
{
/* delConversation, */
fetchAppInfo
,
fetchAppParams
,
fetchChatList
,
fetchConversations
,
fetchSuggestedQuestions
,
pinConversation
,
sendChatMessage
,
stopChatMessageResponding
,
unpinConversation
,
updateFeedback
}
from
'@/service/share'
import
type
{
ConversationItem
,
SiteInfo
}
from
'@/models/share'
import
type
{
PromptConfig
,
SuggestedQuestionsAfterAnswerConfig
}
from
'@/models/debug'
import
type
{
Feedbacktype
,
IChatItem
}
from
'@/app/components/app/chat'
import
Chat
from
'@/app/components/app/chat'
import
{
changeLanguage
}
from
'@/i18n/i18next-config'
// import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import
Loading
from
'@/app/components/base/loading'
import
{
replaceStringWithValues
}
from
'@/app/components/app/configuration/prompt-value-panel'
import
{
userInputsFormToPromptVariables
}
from
'@/utils/model-config'
import
type
{
InstalledApp
}
from
'@/models/explore'
// import Confirm from '@/app/components/base/confirm'
export
type
IMainProps
=
{
isInstalledApp
?:
boolean
installedAppInfo
?:
InstalledApp
}
const
Main
:
FC
<
IMainProps
>
=
({
isInstalledApp
=
false
,
installedAppInfo
,
})
=>
{
const
{
t
}
=
useTranslation
()
// const media = useBreakpoints()
// const isMobile = media === MediaType.mobile
/*
* app info
*/
const
[
appUnavailable
,
setAppUnavailable
]
=
useState
<
boolean
>
(
false
)
const
[
isUnknwonReason
,
setIsUnknwonReason
]
=
useState
<
boolean
>
(
false
)
const
[
appId
,
setAppId
]
=
useState
<
string
>
(
''
)
const
[
isPublicVersion
,
setIsPublicVersion
]
=
useState
<
boolean
>
(
true
)
const
[
siteInfo
,
setSiteInfo
]
=
useState
<
SiteInfo
|
null
>
()
const
[
promptConfig
,
setPromptConfig
]
=
useState
<
PromptConfig
|
null
>
(
null
)
const
[
inited
,
setInited
]
=
useState
<
boolean
>
(
false
)
const
[
plan
,
setPlan
]
=
useState
<
string
>
(
'basic'
)
// basic/plus/pro
// in mobile, show sidebar by click button
const
[
isShowSidebar
,
{
setTrue
:
showSidebar
,
setFalse
:
hideSidebar
}]
=
useBoolean
(
false
)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
useEffect
(()
=>
{
if
(
siteInfo
?.
title
)
{
if
(
plan
!==
'basic'
)
document
.
title
=
`
${
siteInfo
.
title
}
`
else
document
.
title
=
`
${
siteInfo
.
title
}
- Powered by Dify`
}
},
[
siteInfo
?.
title
,
plan
])
/*
* conversation info
*/
const
[
allConversationList
,
setAllConversationList
]
=
useState
<
ConversationItem
[]
>
([])
const
[
isClearConversationList
,
{
setTrue
:
clearConversationListTrue
,
setFalse
:
clearConversationListFalse
}]
=
useBoolean
(
false
)
const
[
isClearPinnedConversationList
,
{
setTrue
:
clearPinnedConversationListTrue
,
setFalse
:
clearPinnedConversationListFalse
}]
=
useBoolean
(
false
)
const
{
conversationList
,
setConversationList
,
pinnedConversationList
,
setPinnedConversationList
,
currConversationId
,
setCurrConversationId
,
getConversationIdFromStorage
,
isNewConversation
,
currConversationInfo
,
currInputs
,
newConversationInputs
,
// existConversationInputs,
resetNewConversationInputs
,
setCurrInputs
,
setNewConversationInfo
,
setExistConversationInfo
,
}
=
useConversation
()
const
[
hasMore
,
setHasMore
]
=
useState
<
boolean
>
(
true
)
const
[
hasPinnedMore
,
setHasPinnedMore
]
=
useState
<
boolean
>
(
true
)
const
onMoreLoaded
=
({
data
:
conversations
,
has_more
}:
any
)
=>
{
setHasMore
(
has_more
)
if
(
isClearConversationList
)
{
setConversationList
(
conversations
)
clearConversationListFalse
()
}
else
{
setConversationList
([...
conversationList
,
...
conversations
])
}
}
const
onPinnedMoreLoaded
=
({
data
:
conversations
,
has_more
}:
any
)
=>
{
setHasPinnedMore
(
has_more
)
if
(
isClearPinnedConversationList
)
{
setPinnedConversationList
(
conversations
)
clearPinnedConversationListFalse
()
}
else
{
setPinnedConversationList
([...
pinnedConversationList
,
...
conversations
])
}
}
const
[
controlUpdateConversationList
,
setControlUpdateConversationList
]
=
useState
(
0
)
const
noticeUpdateList
=
()
=>
{
setHasMore
(
true
)
clearConversationListTrue
()
setHasPinnedMore
(
true
)
clearPinnedConversationListTrue
()
setControlUpdateConversationList
(
Date
.
now
())
}
const
handlePin
=
async
(
id
:
string
)
=>
{
await
pinConversation
(
isInstalledApp
,
installedAppInfo
?.
id
,
id
)
notify
({
type
:
'success'
,
message
:
t
(
'common.api.success'
)
})
noticeUpdateList
()
}
const
handleUnpin
=
async
(
id
:
string
)
=>
{
await
unpinConversation
(
isInstalledApp
,
installedAppInfo
?.
id
,
id
)
notify
({
type
:
'success'
,
message
:
t
(
'common.api.success'
)
})
noticeUpdateList
()
}
const
[
isShowConfirm
,
{
setTrue
:
showConfirm
,
setFalse
:
hideConfirm
}]
=
useBoolean
(
false
)
const
[
toDeleteConversationId
,
setToDeleteConversationId
]
=
useState
(
''
)
const
handleDelete
=
(
id
:
string
)
=>
{
setToDeleteConversationId
(
id
)
hideSidebar
()
// mobile
showConfirm
()
}
// const didDelete = async () => {
// await delConversation(isInstalledApp, installedAppInfo?.id, toDeleteConversationId)
// notify({ type: 'success', message: t('common.api.success') })
// hideConfirm()
// if (currConversationId === toDeleteConversationId)
// handleConversationIdChange('-1')
// noticeUpdateList()
// }
const
[
suggestedQuestionsAfterAnswerConfig
,
setSuggestedQuestionsAfterAnswerConfig
]
=
useState
<
SuggestedQuestionsAfterAnswerConfig
|
null
>
(
null
)
const
[
conversationIdChangeBecauseOfNew
,
setConversationIdChangeBecauseOfNew
,
getConversationIdChangeBecauseOfNew
]
=
useGetState
(
false
)
const
[
isChatStarted
,
{
setTrue
:
setChatStarted
,
setFalse
:
setChatNotStarted
}]
=
useBoolean
(
false
)
const
handleStartChat
=
(
inputs
:
Record
<
string
,
any
>
)
=>
{
createNewChat
()
setConversationIdChangeBecauseOfNew
(
true
)
setCurrInputs
(
inputs
)
setChatStarted
()
// parse variables in introduction
setChatList
(
generateNewChatListWithOpenstatement
(
''
,
inputs
))
}
const
hasSetInputs
=
(()
=>
{
if
(
!
isNewConversation
)
return
true
return
isChatStarted
})()
// const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
const
conversationIntroduction
=
currConversationInfo
?.
introduction
||
''
const
handleConversationSwitch
=
()
=>
{
if
(
!
inited
)
return
if
(
!
appId
)
{
// wait for appId
setTimeout
(
handleConversationSwitch
,
100
)
return
}
// update inputs of current conversation
let
notSyncToStateIntroduction
=
''
let
notSyncToStateInputs
:
Record
<
string
,
any
>
|
undefined
|
null
=
{}
if
(
!
isNewConversation
)
{
const
item
=
allConversationList
.
find
(
item
=>
item
.
id
===
currConversationId
)
notSyncToStateInputs
=
item
?.
inputs
||
{}
setCurrInputs
(
notSyncToStateInputs
)
notSyncToStateIntroduction
=
item
?.
introduction
||
''
setExistConversationInfo
({
name
:
item
?.
name
||
''
,
introduction
:
notSyncToStateIntroduction
,
})
}
else
{
notSyncToStateInputs
=
newConversationInputs
setCurrInputs
(
notSyncToStateInputs
)
}
// update chat list of current conversation
if
(
!
isNewConversation
&&
!
conversationIdChangeBecauseOfNew
&&
!
isResponsing
)
{
fetchChatList
(
currConversationId
,
isInstalledApp
,
installedAppInfo
?.
id
).
then
((
res
:
any
)
=>
{
const
{
data
}
=
res
const
newChatList
:
IChatItem
[]
=
generateNewChatListWithOpenstatement
(
notSyncToStateIntroduction
,
notSyncToStateInputs
)
data
.
forEach
((
item
:
any
)
=>
{
newChatList
.
push
({
id
:
`question-
${
item
.
id
}
`
,
content
:
item
.
query
,
isAnswer
:
false
,
})
newChatList
.
push
({
id
:
item
.
id
,
content
:
item
.
answer
,
feedback
:
item
.
feedback
,
isAnswer
:
true
,
})
})
setChatList
(
newChatList
)
})
}
if
(
isNewConversation
&&
isChatStarted
)
setChatList
(
generateNewChatListWithOpenstatement
())
setControlFocus
(
Date
.
now
())
}
useEffect
(
handleConversationSwitch
,
[
currConversationId
,
inited
])
const
handleConversationIdChange
=
(
id
:
string
)
=>
{
if
(
id
===
'-1'
)
{
createNewChat
()
setConversationIdChangeBecauseOfNew
(
true
)
}
else
{
setConversationIdChangeBecauseOfNew
(
false
)
}
// trigger handleConversationSwitch
setCurrConversationId
(
id
,
appId
)
setIsShowSuggestion
(
false
)
hideSidebar
()
}
/*
* chat info. chat is under conversation.
*/
const
[
chatList
,
setChatList
,
getChatList
]
=
useGetState
<
IChatItem
[]
>
([])
const
chatListDomRef
=
useRef
<
HTMLDivElement
>
(
null
)
useEffect
(()
=>
{
// scroll to bottom
if
(
chatListDomRef
.
current
)
chatListDomRef
.
current
.
scrollTop
=
chatListDomRef
.
current
.
scrollHeight
},
[
chatList
,
currConversationId
])
// user can not edit inputs if user had send message
const
canEditInputs
=
!
chatList
.
some
(
item
=>
item
.
isAnswer
===
false
)
&&
isNewConversation
const
createNewChat
=
async
()
=>
{
// if new chat is already exist, do not create new chat
abortController
?.
abort
()
setResponsingFalse
()
if
(
conversationList
.
some
(
item
=>
item
.
id
===
'-1'
))
return
setConversationList
(
produce
(
conversationList
,
(
draft
)
=>
{
draft
.
unshift
({
id
:
'-1'
,
name
:
t
(
'share.chat.newChatDefaultName'
),
inputs
:
newConversationInputs
,
introduction
:
conversationIntroduction
,
})
}))
}
// sometime introduction is not applied to state
const
generateNewChatListWithOpenstatement
=
(
introduction
?:
string
,
inputs
?:
Record
<
string
,
any
>
|
null
)
=>
{
let
caculatedIntroduction
=
introduction
||
conversationIntroduction
||
''
const
caculatedPromptVariables
=
inputs
||
currInputs
||
null
if
(
caculatedIntroduction
&&
caculatedPromptVariables
)
caculatedIntroduction
=
replaceStringWithValues
(
caculatedIntroduction
,
promptConfig
?.
prompt_variables
||
[],
caculatedPromptVariables
)
// console.log(isPublicVersion)
const
openstatement
=
{
id
:
`
${
Date
.
now
()}
`
,
content
:
caculatedIntroduction
,
isAnswer
:
true
,
feedbackDisabled
:
true
,
isOpeningStatement
:
isPublicVersion
,
}
if
(
caculatedIntroduction
)
return
[
openstatement
]
return
[]
}
const
fetchAllConversations
=
()
=>
{
return
fetchConversations
(
isInstalledApp
,
installedAppInfo
?.
id
,
undefined
,
undefined
,
100
)
}
const
fetchInitData
=
()
=>
{
return
Promise
.
all
([
isInstalledApp
?
{
app_id
:
installedAppInfo
?.
id
,
site
:
{
title
:
installedAppInfo
?.
app
.
name
,
prompt_public
:
false
,
copyright
:
''
,
},
plan
:
'basic'
,
}
:
fetchAppInfo
(),
fetchAllConversations
(),
fetchAppParams
(
isInstalledApp
,
installedAppInfo
?.
id
)])
}
// init
useEffect
(()
=>
{
(
async
()
=>
{
try
{
const
[
appData
,
conversationData
,
appParams
]:
any
=
await
fetchInitData
()
const
{
app_id
:
appId
,
site
:
siteInfo
,
plan
}:
any
=
appData
setAppId
(
appId
)
setPlan
(
plan
)
const
tempIsPublicVersion
=
siteInfo
.
prompt_public
setIsPublicVersion
(
tempIsPublicVersion
)
const
prompt_template
=
''
// handle current conversation id
const
{
data
:
allConversations
}
=
conversationData
as
{
data
:
ConversationItem
[];
has_more
:
boolean
}
const
_conversationId
=
getConversationIdFromStorage
(
appId
)
const
isNotNewConversation
=
allConversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
setAllConversationList
(
allConversations
)
// fetch new conversation info
const
{
user_input_form
,
opening_statement
:
introduction
,
suggested_questions_after_answer
}:
any
=
appParams
const
prompt_variables
=
userInputsFormToPromptVariables
(
user_input_form
)
if
(
siteInfo
.
default_language
)
changeLanguage
(
siteInfo
.
default_language
)
setNewConversationInfo
({
name
:
t
(
'share.chat.newChatDefaultName'
),
introduction
,
})
setSiteInfo
(
siteInfo
as
SiteInfo
)
setPromptConfig
({
prompt_template
,
prompt_variables
,
}
as
PromptConfig
)
setSuggestedQuestionsAfterAnswerConfig
(
suggested_questions_after_answer
)
// setConversationList(conversations as ConversationItem[])
if
(
isNotNewConversation
)
setCurrConversationId
(
_conversationId
,
appId
,
false
)
setInited
(
true
)
}
catch
(
e
:
any
)
{
if
(
e
.
status
===
404
)
{
setAppUnavailable
(
true
)
}
else
{
setIsUnknwonReason
(
true
)
setAppUnavailable
(
true
)
}
}
})()
},
[])
const
[
isResponsing
,
{
setTrue
:
setResponsingTrue
,
setFalse
:
setResponsingFalse
}]
=
useBoolean
(
false
)
const
[
abortController
,
setAbortController
]
=
useState
<
AbortController
|
null
>
(
null
)
const
{
notify
}
=
useContext
(
ToastContext
)
const
logError
=
(
message
:
string
)
=>
{
notify
({
type
:
'error'
,
message
})
}
const
checkCanSend
=
()
=>
{
const
prompt_variables
=
promptConfig
?.
prompt_variables
const
inputs
=
currInputs
if
(
!
inputs
||
!
prompt_variables
||
prompt_variables
?.
length
===
0
)
return
true
let
hasEmptyInput
=
false
const
requiredVars
=
prompt_variables
?.
filter
(({
key
,
name
,
required
})
=>
{
const
res
=
(
!
key
||
!
key
.
trim
())
||
(
!
name
||
!
name
.
trim
())
||
(
required
||
required
===
undefined
||
required
===
null
)
return
res
})
||
[]
// compatible with old version
requiredVars
.
forEach
(({
key
})
=>
{
if
(
hasEmptyInput
)
return
if
(
!
inputs
?.[
key
])
hasEmptyInput
=
true
})
if
(
hasEmptyInput
)
{
logError
(
t
(
'appDebug.errorMessage.valueOfVarRequired'
))
return
false
}
return
!
hasEmptyInput
}
const
[
controlFocus
,
setControlFocus
]
=
useState
(
0
)
const
[
isShowSuggestion
,
setIsShowSuggestion
]
=
useState
(
false
)
const
doShowSuggestion
=
isShowSuggestion
&&
!
isResponsing
const
[
suggestQuestions
,
setSuggestQuestions
]
=
useState
<
string
[]
>
([])
const
[
messageTaskId
,
setMessageTaskId
]
=
useState
(
''
)
const
[
hasStopResponded
,
setHasStopResponded
,
getHasStopResponded
]
=
useGetState
(
false
)
const
handleSend
=
async
(
message
:
string
)
=>
{
if
(
isResponsing
)
{
notify
({
type
:
'info'
,
message
:
t
(
'appDebug.errorMessage.waitForResponse'
)
})
return
}
const
data
=
{
inputs
:
currInputs
,
query
:
message
,
conversation_id
:
isNewConversation
?
null
:
currConversationId
,
}
// qustion
const
questionId
=
`question-
${
Date
.
now
()}
`
const
questionItem
=
{
id
:
questionId
,
content
:
message
,
isAnswer
:
false
,
}
const
placeholderAnswerId
=
`answer-placeholder-
${
Date
.
now
()}
`
const
placeholderAnswerItem
=
{
id
:
placeholderAnswerId
,
content
:
''
,
isAnswer
:
true
,
}
const
newList
=
[...
getChatList
(),
questionItem
,
placeholderAnswerItem
]
setChatList
(
newList
)
// answer
const
responseItem
=
{
id
:
`
${
Date
.
now
()}
`
,
content
:
''
,
isAnswer
:
true
,
}
let
tempNewConversationId
=
''
setHasStopResponded
(
false
)
setResponsingTrue
()
setIsShowSuggestion
(
false
)
sendChatMessage
(
data
,
{
getAbortController
:
(
abortController
)
=>
{
setAbortController
(
abortController
)
},
onData
:
(
message
:
string
,
isFirstMessage
:
boolean
,
{
conversationId
:
newConversationId
,
messageId
,
taskId
}:
any
)
=>
{
responseItem
.
content
=
responseItem
.
content
+
message
responseItem
.
id
=
messageId
if
(
isFirstMessage
&&
newConversationId
)
tempNewConversationId
=
newConversationId
setMessageTaskId
(
taskId
)
// closesure new list is outdated.
const
newListWithAnswer
=
produce
(
getChatList
().
filter
(
item
=>
item
.
id
!==
responseItem
.
id
&&
item
.
id
!==
placeholderAnswerId
),
(
draft
)
=>
{
if
(
!
draft
.
find
(
item
=>
item
.
id
===
questionId
))
draft
.
push
({
...
questionItem
})
draft
.
push
({
...
responseItem
})
})
setChatList
(
newListWithAnswer
)
},
async
onCompleted
(
hasError
?:
boolean
)
{
setResponsingFalse
()
if
(
hasError
)
return
if
(
getConversationIdChangeBecauseOfNew
())
{
const
{
data
:
allConversations
}:
any
=
await
fetchAllConversations
()
setAllConversationList
(
allConversations
)
noticeUpdateList
()
}
setConversationIdChangeBecauseOfNew
(
false
)
resetNewConversationInputs
()
setChatNotStarted
()
setCurrConversationId
(
tempNewConversationId
,
appId
,
true
)
if
(
suggestedQuestionsAfterAnswerConfig
?.
enabled
&&
!
getHasStopResponded
())
{
const
{
data
}:
any
=
await
fetchSuggestedQuestions
(
responseItem
.
id
,
isInstalledApp
,
installedAppInfo
?.
id
)
setSuggestQuestions
(
data
)
setIsShowSuggestion
(
true
)
}
},
onError
()
{
setResponsingFalse
()
// role back placeholder answer
setChatList
(
produce
(
getChatList
(),
(
draft
)
=>
{
draft
.
splice
(
draft
.
findIndex
(
item
=>
item
.
id
===
placeholderAnswerId
),
1
)
}))
},
},
isInstalledApp
,
installedAppInfo
?.
id
)
}
const
handleFeedback
=
async
(
messageId
:
string
,
feedback
:
Feedbacktype
)
=>
{
await
updateFeedback
({
url
:
`/messages/
${
messageId
}
/feedbacks`
,
body
:
{
rating
:
feedback
.
rating
}
},
isInstalledApp
,
installedAppInfo
?.
id
)
const
newChatList
=
chatList
.
map
((
item
)
=>
{
if
(
item
.
id
===
messageId
)
{
return
{
...
item
,
feedback
,
}
}
return
item
})
setChatList
(
newChatList
)
notify
({
type
:
'success'
,
message
:
t
(
'common.api.success'
)
})
}
const
renderSidebar
=
()
=>
{
if
(
!
appId
||
!
siteInfo
||
!
promptConfig
)
return
null
return
(
<
Sidebar
list=
{
conversationList
}
isClearConversationList=
{
isClearConversationList
}
pinnedList=
{
pinnedConversationList
}
isClearPinnedConversationList=
{
isClearPinnedConversationList
}
onMoreLoaded=
{
onMoreLoaded
}
onPinnedMoreLoaded=
{
onPinnedMoreLoaded
}
isNoMore=
{
!
hasMore
}
isPinnedNoMore=
{
!
hasPinnedMore
}
onCurrentIdChange=
{
handleConversationIdChange
}
currentId=
{
currConversationId
}
copyRight=
{
siteInfo
.
copyright
||
siteInfo
.
title
}
isInstalledApp=
{
isInstalledApp
}
installedAppId=
{
installedAppInfo
?.
id
}
siteInfo=
{
siteInfo
}
onPin=
{
handlePin
}
onUnpin=
{
handleUnpin
}
controlUpdateList=
{
controlUpdateConversationList
}
onDelete=
{
handleDelete
}
/>
)
}
if
(
appUnavailable
)
return
<
AppUnavailable
isUnknwonReason=
{
isUnknwonReason
}
/>
if
(
!
appId
||
!
siteInfo
||
!
promptConfig
)
return
<
Loading
type=
'app'
/>
return
(
<
div
className=
'bg-gray-100'
>
<
Header
title=
{
siteInfo
.
title
}
icon=
{
siteInfo
.
icon
||
''
}
icon_background=
{
siteInfo
.
icon_background
}
isEmbedScene=
{
true
}
// isMobile={isMobile}
// onShowSideBar={showSidebar}
// onCreateNewChat={() => handleConversationIdChange('-1')}
/>
<
div
className=
{
'flex bg-white overflow-hidden'
}
>
{
/* sidebar */
}
{
/* {!isMobile && renderSidebar()} */
}
{
/* {isMobile && isShowSidebar && (
<div className='fixed inset-0 z-50'
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
onClick={hideSidebar}
>
<div className='inline-block' onClick={e => e.stopPropagation()}>
{renderSidebar()}
</div>
</div>
)} */
}
{
/* main */
}
<
div
className=
{
cn
(
isInstalledApp
?
s
.
installedApp
:
'h-[calc(100vh_-_3rem)]'
,
'flex-grow flex flex-col overflow-y-auto'
,
)
}
>
<
ConfigScene
// conversationName={conversationName}
hasSetInputs=
{
hasSetInputs
}
isPublicVersion=
{
isPublicVersion
}
siteInfo=
{
siteInfo
}
promptConfig=
{
promptConfig
}
onStartChat=
{
handleStartChat
}
canEditInputs=
{
canEditInputs
}
savedInputs=
{
currInputs
as
Record
<
string
,
any
>
}
onInputsChange=
{
setCurrInputs
}
plan=
{
plan
}
></
ConfigScene
>
{
hasSetInputs
&&
(
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[66px]'
),
'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden'
)
}
>
<
div
className=
'h-full overflow-y-auto'
ref=
{
chatListDomRef
}
>
<
Chat
chatList=
{
chatList
}
onSend=
{
handleSend
}
isHideFeedbackEdit
onFeedback=
{
handleFeedback
}
isResponsing=
{
isResponsing
}
canStopResponsing=
{
!!
messageTaskId
}
abortResponsing=
{
async
()
=>
{
await
stopChatMessageResponding
(
appId
,
messageTaskId
,
isInstalledApp
,
installedAppInfo
?.
id
)
setHasStopResponded
(
true
)
setResponsingFalse
()
}
}
checkCanSend=
{
checkCanSend
}
controlFocus=
{
controlFocus
}
isShowSuggestion=
{
doShowSuggestion
}
suggestionList=
{
suggestQuestions
}
/>
</
div
>
</
div
>)
}
{
/* {isShowConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}
content={t('share.chat.deleteConversation.content')}
isShow={isShowConfirm}
onClose={hideConfirm}
onConfirm={didDelete}
onCancel={hideConfirm}
/>
)} */
}
</
div
>
</
div
>
</
div
>
)
}
export
default
React
.
memo
(
Main
)
web/app/components/share/chatbot/sidebar/app-info/index.tsx
0 → 100644
View file @
a0b3891c
'use client'
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
cn
from
'classnames'
import
{
appDefaultIconBackground
}
from
'@/config/index'
import
AppIcon
from
'@/app/components/base/app-icon'
export
type
IAppInfoProps
=
{
className
?:
string
icon
:
string
icon_background
?:
string
name
:
string
}
const
AppInfo
:
FC
<
IAppInfoProps
>
=
({
className
,
icon
,
icon_background
,
name
,
})
=>
{
return
(
<
div
className=
{
cn
(
className
,
'flex items-center space-x-3'
)
}
>
<
AppIcon
size=
"small"
icon=
{
icon
}
background=
{
icon_background
||
appDefaultIconBackground
}
/>
<
div
className=
'w-0 grow text-sm font-semibold text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'
>
{
name
}
</
div
>
</
div
>
)
}
export
default
React
.
memo
(
AppInfo
)
web/app/components/share/chatbot/sidebar/card.module.css
0 → 100644
View file @
a0b3891c
.card
:hover
{
background
:
linear-gradient
(
0deg
,
rgba
(
235
,
245
,
255
,
0.4
),
rgba
(
235
,
245
,
255
,
0.4
)),
#FFFFFF
;
}
\ No newline at end of file
web/app/components/share/chatbot/sidebar/card.tsx
0 → 100644
View file @
a0b3891c
import
React
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
s
from
'./card.module.css'
type
PropType
=
{
children
:
React
.
ReactNode
text
?:
string
}
function
Card
({
children
,
text
}:
PropType
)
{
const
{
t
}
=
useTranslation
()
return
(
<
div
className=
{
`${s.card} box-border w-full flex flex-col items-start px-4 py-3 rounded-lg border-solid border border-gray-200 cursor-pointer hover:border-primary-300`
}
>
<
div
className=
'text-gray-400 font-medium text-xs mb-2'
>
{
text
??
t
(
'share.chat.powerBy'
)
}
</
div
>
{
children
}
</
div
>
)
}
export
default
Card
web/app/components/share/chatbot/sidebar/index.tsx
0 → 100644
View file @
a0b3891c
import
React
,
{
useEffect
,
useState
}
from
'react'
import
type
{
FC
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
PencilSquareIcon
,
}
from
'@heroicons/react/24/outline'
import
cn
from
'classnames'
import
Button
from
'../../../base/button'
import
List
from
'./list'
import
AppInfo
from
'@/app/components/share/chat/sidebar/app-info'
// import Card from './card'
import
type
{
ConversationItem
,
SiteInfo
}
from
'@/models/share'
import
{
fetchConversations
}
from
'@/service/share'
export
type
ISidebarProps
=
{
copyRight
:
string
currentId
:
string
onCurrentIdChange
:
(
id
:
string
)
=>
void
list
:
ConversationItem
[]
isClearConversationList
:
boolean
pinnedList
:
ConversationItem
[]
isClearPinnedConversationList
:
boolean
isInstalledApp
:
boolean
installedAppId
?:
string
siteInfo
:
SiteInfo
onMoreLoaded
:
(
res
:
{
data
:
ConversationItem
[];
has_more
:
boolean
})
=>
void
onPinnedMoreLoaded
:
(
res
:
{
data
:
ConversationItem
[];
has_more
:
boolean
})
=>
void
isNoMore
:
boolean
isPinnedNoMore
:
boolean
onPin
:
(
id
:
string
)
=>
void
onUnpin
:
(
id
:
string
)
=>
void
controlUpdateList
:
number
onDelete
:
(
id
:
string
)
=>
void
}
const
Sidebar
:
FC
<
ISidebarProps
>
=
({
copyRight
,
currentId
,
onCurrentIdChange
,
list
,
isClearConversationList
,
pinnedList
,
isClearPinnedConversationList
,
isInstalledApp
,
installedAppId
,
siteInfo
,
onMoreLoaded
,
onPinnedMoreLoaded
,
isNoMore
,
isPinnedNoMore
,
onPin
,
onUnpin
,
controlUpdateList
,
onDelete
,
})
=>
{
const
{
t
}
=
useTranslation
()
const
[
hasPinned
,
setHasPinned
]
=
useState
(
false
)
const
checkHasPinned
=
async
()
=>
{
const
{
data
}:
any
=
await
fetchConversations
(
isInstalledApp
,
installedAppId
,
undefined
,
true
)
setHasPinned
(
data
.
length
>
0
)
}
useEffect
(()
=>
{
checkHasPinned
()
},
[])
useEffect
(()
=>
{
if
(
controlUpdateList
!==
0
)
checkHasPinned
()
},
[
controlUpdateList
])
const
maxListHeight
=
isInstalledApp
?
'max-h-[30vh]'
:
'max-h-[40vh]'
return
(
<
div
className=
{
cn
(
isInstalledApp
?
'tablet:h-[calc(100vh_-_74px)]'
:
'tablet:h-[calc(100vh_-_3rem)]'
,
'shrink-0 flex flex-col bg-white pc:w-[244px] tablet:w-[192px] mobile:w-[240px] border-r border-gray-200 mobile:h-screen'
,
)
}
>
{
isInstalledApp
&&
(
<
AppInfo
className=
'my-4 px-4'
name=
{
siteInfo
.
title
||
''
}
icon=
{
siteInfo
.
icon
||
''
}
icon_background=
{
siteInfo
.
icon_background
}
/>
)
}
<
div
className=
"flex flex-shrink-0 p-4 !pb-0"
>
<
Button
onClick=
{
()
=>
{
onCurrentIdChange
(
'-1'
)
}
}
className=
"group block w-full flex-shrink-0 !justify-start !h-9 text-primary-600 items-center text-sm"
>
<
PencilSquareIcon
className=
"mr-2 h-4 w-4"
/>
{
t
(
'share.chat.newChat'
)
}
</
Button
>
</
div
>
<
div
className=
{
'flex-grow flex flex-col h-0 overflow-y-auto overflow-x-hidden'
}
>
{
/* pinned list */
}
{
hasPinned
&&
(
<
div
className=
{
cn
(
'mt-4 px-4'
,
list
.
length
===
0
&&
'flex flex-col flex-grow'
)
}
>
<
div
className=
'mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'
>
{
t
(
'share.chat.pinnedTitle'
)
}
</
div
>
<
List
className=
{
cn
(
list
.
length
>
0
?
maxListHeight
:
'flex-grow'
)
}
currentId=
{
currentId
}
onCurrentIdChange=
{
onCurrentIdChange
}
list=
{
pinnedList
}
isClearConversationList=
{
isClearPinnedConversationList
}
isInstalledApp=
{
isInstalledApp
}
installedAppId=
{
installedAppId
}
onMoreLoaded=
{
onPinnedMoreLoaded
}
isNoMore=
{
isPinnedNoMore
}
isPinned=
{
true
}
onPinChanged=
{
id
=>
onUnpin
(
id
)
}
controlUpdate=
{
controlUpdateList
+
1
}
onDelete=
{
onDelete
}
/>
</
div
>
)
}
{
/* unpinned list */
}
<
div
className=
{
cn
(
'mt-4 px-4'
,
!
hasPinned
&&
'flex flex-col flex-grow'
)
}
>
{
(
hasPinned
&&
list
.
length
>
0
)
&&
(
<
div
className=
'mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'
>
{
t
(
'share.chat.unpinnedTitle'
)
}
</
div
>
)
}
<
List
className=
{
cn
(
hasPinned
?
maxListHeight
:
'flex-grow'
)
}
currentId=
{
currentId
}
onCurrentIdChange=
{
onCurrentIdChange
}
list=
{
list
}
isClearConversationList=
{
isClearConversationList
}
isInstalledApp=
{
isInstalledApp
}
installedAppId=
{
installedAppId
}
onMoreLoaded=
{
onMoreLoaded
}
isNoMore=
{
isNoMore
}
isPinned=
{
false
}
onPinChanged=
{
id
=>
onPin
(
id
)
}
controlUpdate=
{
controlUpdateList
+
1
}
onDelete=
{
onDelete
}
/>
</
div
>
</
div
>
<
div
className=
"flex flex-shrink-0 pr-4 pb-4 pl-4"
>
<
div
className=
"text-gray-400 font-normal text-xs"
>
©
{
copyRight
}
{
(
new
Date
()).
getFullYear
()
}
</
div
>
</
div
>
</
div
>
)
}
export
default
React
.
memo
(
Sidebar
)
web/app/components/share/chatbot/sidebar/list/index.tsx
0 → 100644
View file @
a0b3891c
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useRef
}
from
'react'
import
{
ChatBubbleOvalLeftEllipsisIcon
,
}
from
'@heroicons/react/24/outline'
import
{
useInfiniteScroll
}
from
'ahooks'
import
{
ChatBubbleOvalLeftEllipsisIcon
as
ChatBubbleOvalLeftEllipsisSolidIcon
}
from
'@heroicons/react/24/solid'
import
cn
from
'classnames'
import
s
from
'./style.module.css'
import
type
{
ConversationItem
}
from
'@/models/share'
import
{
fetchConversations
}
from
'@/service/share'
import
ItemOperation
from
'@/app/components/explore/item-operation'
export
type
IListProps
=
{
className
:
string
currentId
:
string
onCurrentIdChange
:
(
id
:
string
)
=>
void
list
:
ConversationItem
[]
isClearConversationList
:
boolean
isInstalledApp
:
boolean
installedAppId
?:
string
onMoreLoaded
:
(
res
:
{
data
:
ConversationItem
[];
has_more
:
boolean
})
=>
void
isNoMore
:
boolean
isPinned
:
boolean
onPinChanged
:
(
id
:
string
)
=>
void
controlUpdate
:
number
onDelete
:
(
id
:
string
)
=>
void
}
const
List
:
FC
<
IListProps
>
=
({
className
,
currentId
,
onCurrentIdChange
,
list
,
isClearConversationList
,
isInstalledApp
,
installedAppId
,
onMoreLoaded
,
isNoMore
,
isPinned
,
onPinChanged
,
controlUpdate
,
onDelete
,
})
=>
{
const
listRef
=
useRef
<
HTMLDivElement
>
(
null
)
useInfiniteScroll
(
async
()
=>
{
if
(
!
isNoMore
)
{
const
lastId
=
!
isClearConversationList
?
list
[
list
.
length
-
1
]?.
id
:
undefined
const
{
data
:
conversations
,
has_more
}:
any
=
await
fetchConversations
(
isInstalledApp
,
installedAppId
,
lastId
,
isPinned
)
onMoreLoaded
({
data
:
conversations
,
has_more
})
}
return
{
list
:
[]
}
},
{
target
:
listRef
,
isNoMore
:
()
=>
{
return
isNoMore
},
reloadDeps
:
[
isNoMore
,
controlUpdate
],
},
)
return
(
<
nav
ref=
{
listRef
}
className=
{
cn
(
className
,
'shrink-0 space-y-1 bg-white pb-[85px] overflow-y-auto'
)
}
>
{
list
.
map
((
item
)
=>
{
const
isCurrent
=
item
.
id
===
currentId
const
ItemIcon
=
isCurrent
?
ChatBubbleOvalLeftEllipsisSolidIcon
:
ChatBubbleOvalLeftEllipsisIcon
return
(
<
div
onClick=
{
()
=>
onCurrentIdChange
(
item
.
id
)
}
key=
{
item
.
id
}
className=
{
cn
(
s
.
item
,
isCurrent
?
'bg-primary-50 text-primary-600'
:
'text-gray-700 hover:bg-gray-200 hover:text-gray-700'
,
'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer'
,
)
}
>
<
div
className=
'flex items-center w-0 grow'
>
<
ItemIcon
className=
{
cn
(
isCurrent
?
'text-primary-600'
:
'text-gray-400 group-hover:text-gray-500'
,
'mr-3 h-5 w-5 flex-shrink-0'
,
)
}
aria
-
hidden=
"true"
/>
<
span
>
{
item
.
name
}
</
span
>
</
div
>
{
item
.
id
!==
'-1'
&&
(
<
div
className=
{
cn
(
s
.
opBtn
,
'shrink-0'
)
}
onClick=
{
e
=>
e
.
stopPropagation
()
}
>
<
ItemOperation
isPinned=
{
isPinned
}
togglePin=
{
()
=>
onPinChanged
(
item
.
id
)
}
isShowDelete
onDelete=
{
()
=>
onDelete
(
item
.
id
)
}
/>
</
div
>
)
}
</
div
>
)
})
}
</
nav
>
)
}
export
default
React
.
memo
(
List
)
web/app/components/share/chatbot/sidebar/list/style.module.css
0 → 100644
View file @
a0b3891c
.opBtn
{
visibility
:
hidden
;
}
.item
:hover
.opBtn
{
visibility
:
visible
;
}
\ No newline at end of file
web/app/components/share/chatbot/style.module.css
0 → 100644
View file @
a0b3891c
.installedApp
{
height
:
calc
(
100vh
-
74px
);
}
\ No newline at end of file
web/app/components/share/chatbot/value-panel/index.tsx
0 → 100644
View file @
a0b3891c
'use client'
import
type
{
FC
,
ReactNode
}
from
'react'
import
React
from
'react'
import
cn
from
'classnames'
import
{
useTranslation
}
from
'react-i18next'
import
s
from
'./style.module.css'
import
{
StarIcon
}
from
'@/app/components/share/chatbot/welcome/massive-component'
import
Button
from
'@/app/components/base/button'
export
type
ITemplateVarPanelProps
=
{
className
?:
string
header
:
ReactNode
children
?:
ReactNode
|
null
isFold
:
boolean
}
const
TemplateVarPanel
:
FC
<
ITemplateVarPanelProps
>
=
({
className
,
header
,
children
,
isFold
,
})
=>
{
return
(
<
div
className=
{
cn
(
isFold
?
'border border-indigo-100'
:
s
.
boxShodow
,
className
,
'rounded-xl '
)
}
>
{
/* header */
}
<
div
className=
{
cn
(
isFold
&&
'rounded-b-xl'
,
'rounded-t-xl px-6 py-4 bg-indigo-25 text-xs'
)
}
>
{
header
}
</
div
>
{
/* body */
}
{
!
isFold
&&
children
&&
(
<
div
className=
'rounded-b-xl p-6'
>
{
children
}
</
div
>
)
}
</
div
>
)
}
export
const
PanelTitle
:
FC
<
{
title
:
string
;
className
?:
string
}
>
=
({
title
,
className
,
})
=>
{
return
(
<
div
className=
{
cn
(
className
,
'flex items-center space-x-1 text-indigo-600'
)
}
>
<
StarIcon
/>
<
span
className=
'text-xs'
>
{
title
}
</
span
>
</
div
>
)
}
export
const
VarOpBtnGroup
:
FC
<
{
className
?:
string
;
onConfirm
:
()
=>
void
;
onCancel
:
()
=>
void
}
>
=
({
className
,
onConfirm
,
onCancel
,
})
=>
{
const
{
t
}
=
useTranslation
()
return
(
<
div
className=
{
cn
(
className
,
'flex mt-3 space-x-2 mobile:ml-0 tablet:ml-[128px] text-sm'
)
}
>
<
Button
className=
'text-sm'
type=
'primary'
onClick=
{
onConfirm
}
>
{
t
(
'common.operation.save'
)
}
</
Button
>
<
Button
className=
'text-sm'
onClick=
{
onCancel
}
>
{
t
(
'common.operation.cancel'
)
}
</
Button
>
</
div
>
)
}
export
default
React
.
memo
(
TemplateVarPanel
)
web/app/components/share/chatbot/value-panel/style.module.css
0 → 100644
View file @
a0b3891c
.boxShodow
{
box-shadow
:
0px
12px
16px
-4px
rgba
(
16
,
24
,
40
,
0.08
),
0px
4px
6px
-2px
rgba
(
16
,
24
,
40
,
0.03
);
}
\ No newline at end of file
web/app/components/share/chatbot/welcome/icons/logo.png
0 → 100644
View file @
a0b3891c
3.83 KB
web/app/components/share/chatbot/welcome/index.tsx
0 → 100644
View file @
a0b3891c
'use client'
import
type
{
FC
}
from
'react'
import
React
,
{
useEffect
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useContext
}
from
'use-context-selector'
import
TemplateVarPanel
,
{
PanelTitle
,
VarOpBtnGroup
}
from
'../value-panel'
import
s
from
'./style.module.css'
import
{
AppInfo
,
ChatBtn
,
EditBtn
,
FootLogo
,
PromptTemplate
}
from
'./massive-component'
import
type
{
SiteInfo
}
from
'@/models/share'
import
type
{
PromptConfig
}
from
'@/models/debug'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
Select
from
'@/app/components/base/select'
import
{
DEFAULT_VALUE_MAX_LEN
}
from
'@/config'
// regex to match the {{}} and replace it with a span
const
regex
=
/
\{\{([^
}
]
+
)\}\}
/g
export
type
IWelcomeProps
=
{
// conversationName: string
hasSetInputs
:
boolean
isPublicVersion
:
boolean
siteInfo
:
SiteInfo
promptConfig
:
PromptConfig
onStartChat
:
(
inputs
:
Record
<
string
,
any
>
)
=>
void
canEditInputs
:
boolean
savedInputs
:
Record
<
string
,
any
>
onInputsChange
:
(
inputs
:
Record
<
string
,
any
>
)
=>
void
plan
:
string
}
const
Welcome
:
FC
<
IWelcomeProps
>
=
({
// conversationName,
hasSetInputs
,
isPublicVersion
,
siteInfo
,
plan
,
promptConfig
,
onStartChat
,
canEditInputs
,
savedInputs
,
onInputsChange
,
})
=>
{
const
{
t
}
=
useTranslation
()
const
hasVar
=
promptConfig
.
prompt_variables
.
length
>
0
const
[
isFold
,
setIsFold
]
=
useState
<
boolean
>
(
true
)
const
[
inputs
,
setInputs
]
=
useState
<
Record
<
string
,
any
>>
((()
=>
{
if
(
hasSetInputs
)
return
savedInputs
const
res
:
Record
<
string
,
any
>
=
{}
if
(
promptConfig
)
{
promptConfig
.
prompt_variables
.
forEach
((
item
)
=>
{
res
[
item
.
key
]
=
''
})
}
// debugger
return
res
})())
useEffect
(()
=>
{
if
(
!
savedInputs
)
{
const
res
:
Record
<
string
,
any
>
=
{}
if
(
promptConfig
)
{
promptConfig
.
prompt_variables
.
forEach
((
item
)
=>
{
res
[
item
.
key
]
=
''
})
}
setInputs
(
res
)
}
else
{
setInputs
(
savedInputs
)
}
},
[
savedInputs
])
const
highLightPromoptTemplate
=
(()
=>
{
if
(
!
promptConfig
)
return
''
const
res
=
promptConfig
.
prompt_template
.
replace
(
regex
,
(
match
,
p1
)
=>
{
return
`<span class='text-gray-800 font-bold'>
${
inputs
?.[
p1
]
?
inputs
?.[
p1
]
:
match
}<
/
span
>
`
})
return res
})()
const
{
notify
}
= useContext(ToastContext)
const logError = (message: string) =
>
{
notify
({
type
:
'error'
,
message
,
duration
:
3000
})
}
// const renderHeader = () =
>
{
// return (
// <div className='absolute top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'>
// <div className='text-gray-900'>
{
conversationName
}<
/
div
>
//
</
div
>
// )
//
}
const renderInputs = () =
>
{
return
(
<
div
className=
'space-y-3'
>
{
promptConfig
.
prompt_variables
.
map
(
item
=>
(
<
div
className=
'tablet:flex tablet:!h-9 mobile:space-y-2 tablet:space-y-0 mobile:text-xs tablet:text-sm'
key=
{
item
.
key
}
>
<
label
className=
{
`flex-shrink-0 flex items-center mobile:text-gray-700 tablet:text-gray-900 mobile:font-medium pc:font-normal ${s.formLabel}`
}
>
{
item
.
name
}
</
label
>
{
item
.
type
===
'select'
?
(
<
Select
className=
'w-full'
defaultValue=
{
inputs
?.[
item
.
key
]
}
onSelect=
{
(
i
)
=>
{
setInputs
({
...
inputs
,
[
item
.
key
]:
i
.
value
})
}
}
items=
{
(
item
.
options
||
[]).
map
(
i
=>
({
name
:
i
,
value
:
i
}))
}
allowSearch=
{
false
}
bgClassName=
'bg-gray-50'
/>
)
:
(
<
input
placeholder=
{
`${item.name}${!item.required ? `
(
$
{
t
(
'appDebug.variableTable.optional'
)})
` : ''}`
}
value=
{
inputs
?.[
item
.
key
]
||
''
}
onChange=
{
(
e
)
=>
{
setInputs
({
...
inputs
,
[
item
.
key
]:
e
.
target
.
value
})
}
}
className=
{
'w-full flex-grow py-2 pl-3 pr-3 box-border rounded-lg bg-gray-50'
}
maxLength=
{
item
.
max_length
||
DEFAULT_VALUE_MAX_LEN
}
/>
)
}
</
div
>
))
}
</
div
>
)
}
const canChat = () =
>
{
const
prompt_variables
=
promptConfig
?.
prompt_variables
if
(
!
inputs
||
!
prompt_variables
||
prompt_variables
?.
length
===
0
)
return
true
let
hasEmptyInput
=
false
const
requiredVars
=
prompt_variables
?.
filter
(({
key
,
name
,
required
})
=>
{
const
res
=
(
!
key
||
!
key
.
trim
())
||
(
!
name
||
!
name
.
trim
())
||
(
required
||
required
===
undefined
||
required
===
null
)
return
res
})
||
[]
// compatible with old version
requiredVars
.
forEach
(({
key
})
=>
{
if
(
hasEmptyInput
)
return
if
(
!
inputs
?.[
key
])
hasEmptyInput
=
true
})
if
(
hasEmptyInput
)
{
logError
(
t
(
'appDebug.errorMessage.valueOfVarRequired'
))
return
false
}
return
!
hasEmptyInput
}
const handleChat = () =
>
{
if
(
!
canChat
())
return
onStartChat
(
inputs
)
}
const renderNoVarPanel = () =
>
{
if
(
isPublicVersion
)
{
return
(
<
div
>
<
AppInfo
siteInfo=
{
siteInfo
}
/>
<
TemplateVarPanel
isFold=
{
false
}
header=
{
<>
<
PanelTitle
title=
{
t
(
'share.chat.publicPromptConfigTitle'
)
}
className=
'mb-1'
/>
<
PromptTemplate
html=
{
highLightPromoptTemplate
}
/>
</>
}
>
<
ChatBtn
onClick=
{
handleChat
}
/>
</
TemplateVarPanel
>
</
div
>
)
}
// private version
return
(
<
TemplateVarPanel
isFold=
{
false
}
header=
{
<
AppInfo
siteInfo=
{
siteInfo
}
/>
}
>
<
ChatBtn
onClick=
{
handleChat
}
/>
</
TemplateVarPanel
>
)
}
const renderVarPanel = () =
>
{
return
(
<
TemplateVarPanel
isFold=
{
false
}
header=
{
<
AppInfo
siteInfo=
{
siteInfo
}
/>
}
>
{
renderInputs
()
}
<
ChatBtn
className=
'mt-3 mobile:ml-0 tablet:ml-[128px]'
onClick=
{
handleChat
}
/>
</
TemplateVarPanel
>
)
}
const renderVarOpBtnGroup = () =
>
{
return
(
<
VarOpBtnGroup
onConfirm=
{
()
=>
{
if
(
!
canChat
())
return
onInputsChange
(
inputs
)
setIsFold
(
true
)
}
}
onCancel=
{
()
=>
{
setInputs
(
savedInputs
)
setIsFold
(
true
)
}
}
/>
)
}
const renderHasSetInputsPublic = () =
>
{
if
(
!
canEditInputs
)
{
return
(
<
TemplateVarPanel
isFold=
{
false
}
header=
{
<>
<
PanelTitle
title=
{
t
(
'share.chat.publicPromptConfigTitle'
)
}
className=
'mb-1'
/>
<
PromptTemplate
html=
{
highLightPromoptTemplate
}
/>
</>
}
/>
)
}
return
(
<
TemplateVarPanel
isFold=
{
isFold
}
header=
{
<>
<
PanelTitle
title=
{
t
(
'share.chat.publicPromptConfigTitle'
)
}
className=
'mb-1'
/>
<
PromptTemplate
html=
{
highLightPromoptTemplate
}
/>
{
isFold
&&
(
<
div
className=
'flex items-center justify-between mt-3 border-t border-indigo-100 pt-4 text-xs text-indigo-600'
>
<
span
className=
'text-gray-700'
>
{
t
(
'share.chat.configStatusDes'
)
}
</
span
>
<
EditBtn
onClick=
{
()
=>
setIsFold
(
false
)
}
/>
</
div
>
)
}
</>
}
>
{
renderInputs
()
}
{
renderVarOpBtnGroup
()
}
</
TemplateVarPanel
>
)
}
const renderHasSetInputsPrivate = () =
>
{
if
(
!
canEditInputs
||
!
hasVar
)
return
null
return
(
<
TemplateVarPanel
isFold=
{
isFold
}
header=
{
<
div
className=
'flex items-center justify-between text-indigo-600'
>
<
PanelTitle
title=
{
!
isFold
?
t
(
'share.chat.privatePromptConfigTitle'
)
:
t
(
'share.chat.configStatusDes'
)
}
/>
{
isFold
&&
(
<
EditBtn
onClick=
{
()
=>
setIsFold
(
false
)
}
/>
)
}
</
div
>
}
>
{
renderInputs
()
}
{
renderVarOpBtnGroup
()
}
</
TemplateVarPanel
>
)
}
const renderHasSetInputs = () =
>
{
if
((
!
isPublicVersion
&&
!
canEditInputs
)
||
!
hasVar
)
return
null
return
(
<
div
className=
'pt-[88px] mb-5'
>
{
isPublicVersion
?
renderHasSetInputsPublic
()
:
renderHasSetInputsPrivate
()
}
</
div
>)
}
return (
<
div
className=
'relative mobile:min-h-[48px] tablet:min-h-[64px]'
>
{
/* {hasSetInputs && renderHeader()} */
}
<
div
className=
'mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'
>
{
/* Has't set inputs */
}
{
!
hasSetInputs
&&
(
<
div
className=
'mobile:pt-[72px] tablet:pt-[128px] pc:pt-[200px]'
>
{
hasVar
?
(
renderVarPanel
()
)
:
(
renderNoVarPanel
()
)
}
</
div
>
)
}
{
/* Has set inputs */
}
{
hasSetInputs
&&
renderHasSetInputs
()
}
{
/* foot */
}
{
!
hasSetInputs
&&
(
<
div
className=
'mt-4 flex justify-between items-center h-8 text-xs text-gray-400'
>
{
siteInfo
.
privacy_policy
?
<
div
>
{
t
(
'share.chat.privacyPolicyLeft'
)
}
<
a
className=
'text-gray-500'
href=
{
siteInfo
.
privacy_policy
}
target=
'_blank'
>
{
t
(
'share.chat.privacyPolicyMiddle'
)
}
</
a
>
{
t
(
'share.chat.privacyPolicyRight'
)
}
</
div
>
:
<
div
>
</
div
>
}
{
plan
===
'basic'
&&
<
a
className=
'flex items-center pr-3 space-x-3'
href=
"https://dify.ai/"
target=
"_blank"
>
<
span
className=
'uppercase'
>
{
t
(
'share.chat.powerBy'
)
}
</
span
>
<
FootLogo
/>
</
a
>
}
</
div
>
)
}
</
div
>
</
div
>
)
}
export default React.memo(Welcome)
web/app/components/share/chatbot/welcome/massive-component.tsx
0 → 100644
View file @
a0b3891c
'use client'
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
cn
from
'classnames'
import
{
useTranslation
}
from
'react-i18next'
import
{
PencilIcon
,
}
from
'@heroicons/react/24/solid'
import
s
from
'./style.module.css'
import
type
{
SiteInfo
}
from
'@/models/share'
import
Button
from
'@/app/components/base/button'
export
const
AppInfo
:
FC
<
{
siteInfo
:
SiteInfo
}
>
=
({
siteInfo
})
=>
{
const
{
t
}
=
useTranslation
()
return
(
<
div
>
<
div
className=
'flex items-center py-2 text-xl font-medium text-gray-700 rounded-md'
>
👏
{
t
(
'share.common.welcome'
)
}
{
siteInfo
.
title
}
</
div
>
<
p
className=
'text-sm text-gray-500'
>
{
siteInfo
.
description
}
</
p
>
</
div
>
)
}
export
const
PromptTemplate
:
FC
<
{
html
:
string
}
>
=
({
html
})
=>
{
return
(
<
div
className=
{
' box-border text-sm text-gray-700'
}
dangerouslySetInnerHTML=
{
{
__html
:
html
}
}
></
div
>
)
}
export
const
StarIcon
=
()
=>
(
<
svg
width=
"12"
height=
"12"
viewBox=
"0 0 12 12"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
path
d=
"M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z"
fill=
"#444CE7"
/>
<
path
d=
"M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z"
fill=
"#444CE7"
/>
<
path
d=
"M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z"
fill=
"#444CE7"
/>
</
svg
>
)
export
const
ChatBtn
:
FC
<
{
onClick
:
()
=>
void
;
className
?:
string
}
>
=
({
className
,
onClick
,
})
=>
{
const
{
t
}
=
useTranslation
()
return
(
<
Button
type=
'primary'
className=
{
cn
(
className
,
`!p-0 space-x-2 flex items-center ${s.customBtn}`
)
}
onClick=
{
onClick
}
>
<
svg
width=
"20"
height=
"21"
viewBox=
"0 0 20 21"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
path
fillRule=
"evenodd"
clipRule=
"evenodd"
d=
"M18 10.5C18 14.366 14.418 17.5 10 17.5C8.58005 17.506 7.17955 17.1698 5.917 16.52L2 17.5L3.338 14.377C2.493 13.267 2 11.934 2 10.5C2 6.634 5.582 3.5 10 3.5C14.418 3.5 18 6.634 18 10.5ZM7 9.5H5V11.5H7V9.5ZM15 9.5H13V11.5H15V9.5ZM9 9.5H11V11.5H9V9.5Z"
fill=
"white"
/>
</
svg
>
{
t
(
'share.chat.startChat'
)
}
</
Button
>
)
}
export
const
EditBtn
=
({
className
,
onClick
}:
{
className
?:
string
;
onClick
:
()
=>
void
})
=>
{
const
{
t
}
=
useTranslation
()
return
(
<
div
className=
{
cn
(
'px-2 flex space-x-1 items-center rounded-md cursor-pointer'
,
className
)
}
onClick=
{
onClick
}
>
<
PencilIcon
className=
'w-3 h-3'
/>
<
span
>
{
t
(
'common.operation.edit'
)
}
</
span
>
</
div
>
)
}
export
const
FootLogo
=
()
=>
(
<
div
className=
{
s
.
logo
}
/>
)
web/app/components/share/chatbot/welcome/style.module.css
0 → 100644
View file @
a0b3891c
.boxShodow
{
box-shadow
:
0px
12px
16px
-4px
rgba
(
16
,
24
,
40
,
0.08
),
0px
4px
6px
-2px
rgba
(
16
,
24
,
40
,
0.03
);
}
.bgGrayColor
{
background-color
:
#F9FAFB
;
}
.headerBg
{
height
:
3.5rem
;
padding-left
:
1.5rem
;
padding-right
:
1.5rem
;
}
.formLabel
{
width
:
120px
;
margin-right
:
8px
;
}
.customBtn
{
width
:
136px
;
}
.logo
{
width
:
48px
;
height
:
20px
;
background
:
url(./icons/logo.png)
center
center
no-repeat
;
background-size
:
contain
;
}
\ No newline at end of file
web/app/components/share/header.tsx
View file @
a0b3891c
import
type
{
FC
}
from
'react'
import
React
from
'react'
import
AppIcon
from
'@/app/components/base/app-icon'
import
{
Bars3Icon
,
PencilSquareIcon
,
}
from
'@heroicons/react/24/solid'
import
AppIcon
from
'@/app/components/base/app-icon'
export
type
IHeaderProps
=
{
title
:
string
icon
:
string
icon_background
:
string
isMobile
?:
boolean
isEmbedScene
?:
boolean
onShowSideBar
?:
()
=>
void
onCreateNewChat
?:
()
=>
void
}
...
...
@@ -18,29 +19,34 @@ const Header: FC<IHeaderProps> = ({
isMobile
,
icon
,
icon_background
,
isEmbedScene
=
false
,
onShowSideBar
,
onCreateNewChat
,
})
=>
{
return
(
<
div
className=
"shrink-0 flex items-center justify-between h-12 px-3 bg-gray-100"
>
{
isMobile
?
(
<
div
className=
'flex items-center justify-center h-8 w-8 cursor-pointer'
onClick=
{
()
=>
onShowSideBar
?.()
}
>
<
Bars3Icon
className=
"h-4 w-4 text-gray-500"
/>
</
div
>
)
:
<
div
></
div
>
}
<
div
className=
{
`shrink-0 flex items-center justify-between h-12 px-3 bg-gray-100 ${isEmbedScene ? 'bg-gradient-to-r from-blue-600 to-sky-500' : ''}`
}
>
{
isMobile
?
(
<
div
className=
'flex items-center justify-center h-8 w-8 cursor-pointer'
onClick=
{
()
=>
onShowSideBar
?.()
}
>
<
Bars3Icon
className=
"h-4 w-4 text-gray-500"
/>
</
div
>
)
:
<
div
></
div
>
}
<
div
className=
'flex items-center space-x-2'
>
<
AppIcon
size=
"small"
icon=
{
icon
}
background=
{
icon_background
}
/>
<
div
className=
" text-sm text-gray-800 font-bold"
>
{
title
}
</
div
>
<
div
className=
{
`text-sm text-gray-800 font-bold ${isEmbedScene ? 'text-white' : ''}`
}
>
{
title
}
</
div
>
</
div
>
{
isMobile
?
(
<
div
className=
'flex items-center justify-center h-8 w-8 cursor-pointer'
onClick=
{
()
=>
onCreateNewChat
?.()
}
>
<
PencilSquareIcon
className=
"h-4 w-4 text-gray-500"
/>
</
div
>)
:
<
div
></
div
>
}
{
isMobile
?
(
<
div
className=
'flex items-center justify-center h-8 w-8 cursor-pointer'
onClick=
{
()
=>
onCreateNewChat
?.()
}
>
<
PencilSquareIcon
className=
"h-4 w-4 text-gray-500"
/>
</
div
>)
:
<
div
></
div
>
}
</
div
>
)
}
...
...
web/i18n/lang/app-overview.en.ts
View file @
a0b3891c
...
...
@@ -42,6 +42,8 @@ const translation = {
explanation
:
'Choose the way to embed chat app to your website'
,
iframe
:
'To add the chat app any where on your website, add this iframe to your html code.'
,
scripts
:
'To add a chat app to the bottom right of your website add this code to your html.'
,
copied
:
'Copied'
,
copy
:
'Copy'
,
},
customize
:
{
way
:
'way'
,
...
...
web/i18n/lang/app-overview.zh.ts
View file @
a0b3891c
...
...
@@ -42,6 +42,8 @@ const translation = {
explanation
:
'选择一种方式将聊天应用嵌入到你的网站中'
,
iframe
:
'将以下 iframe 嵌入到你的网站中的目标位置'
,
scripts
:
'将以下代码嵌入到你的网站中'
,
copied
:
'已复制'
,
copy
:
'复制'
,
},
customize
:
{
way
:
'方法'
,
...
...
web/public/embed.js
0 → 100644
View file @
a0b3891c
/** this file is used to embed the chatbot in a website
* the difyChatbotConfig should be defined in the html file before this script is included
* the difyChatbotConfig should contain the token of the chatbot
* the token can be found in the chatbot settings page
*/
// attention: This JavaScript script must be placed after the <body> element. Otherwise, the script will not work.
document
.
body
.
onload
=
embedChatbot
;
async
function
embedChatbot
()
{
const
difyChatbotConfig
=
window
.
difyChatbotConfig
;
if
(
!
difyChatbotConfig
||
!
difyChatbotConfig
.
token
)
{
console
.
error
(
'difyChatbotConfig is empty or token is not provided'
)
return
;
}
const
isDev
=
!!
difyChatbotConfig
.
isDev
const
openIcon
=
`<svg
id="openIcon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z"
fill="white"
/>
</svg>`
const
closeIcon
=
`<svg
id="closeIcon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 18L6 6M6 18L18 6"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>`
// create iframe
function
createIframe
()
{
const
iframe
=
document
.
createElement
(
'iframe'
);
iframe
.
allow
=
"fullscreen"
iframe
.
id
=
'dify-chatbot-bubble-window'
iframe
.
src
=
`https://
${
isDev
?
'dev.'
:
''
}
udify.app/chatbot/
${
difyChatbotConfig
.
token
}
`
;
iframe
.
style
.
cssText
=
'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset;'
document
.
body
.
appendChild
(
iframe
);
}
const
targetButton
=
document
.
getElementById
(
'dify-chatbot-bubble-button'
)
if
(
!
targetButton
)
{
// create button
const
containerDiv
=
document
.
createElement
(
"div"
);
containerDiv
.
id
=
'dify-chatbot-bubble-button'
containerDiv
.
style
.
cssText
=
`position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}`
;
const
displayDiv
=
document
.
createElement
(
'div'
);
displayDiv
.
style
.
cssText
=
'display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;'
;
displayDiv
.
innerHTML
=
openIcon
containerDiv
.
appendChild
(
displayDiv
);
document
.
body
.
appendChild
(
containerDiv
);
// add click event to control iframe display
containerDiv
.
addEventListener
(
'click'
,
function
()
{
const
targetIframe
=
document
.
getElementById
(
'dify-chatbot-bubble-window'
)
if
(
!
targetIframe
)
{
createIframe
()
return
;
}
if
(
targetIframe
.
style
.
display
===
'none'
)
{
targetIframe
.
style
.
display
=
'block'
;
displayDiv
.
innerHTML
=
closeIcon
}
else
{
targetIframe
.
style
.
display
=
'none'
;
displayDiv
.
innerHTML
=
openIcon
}
});
}
}
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