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
ec261aea
Unverified
Commit
ec261aea
authored
Jun 28, 2023
by
Joel
Committed by
GitHub
Jun 28, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: conversation app support pin and delete conversation (#467)
parent
accc5faa
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
374 additions
and
147 deletions
+374
-147
index.tsx
web/app/components/base/confirm-ui/index.tsx
+1
-1
index.tsx
web/app/components/explore/item-operation/index.tsx
+2
-2
use-conversation.ts
web/app/components/share/chat/hooks/use-conversation.ts
+10
-6
index.tsx
web/app/components/share/chat/index.tsx
+76
-16
index.tsx
web/app/components/share/chat/sidebar/index.tsx
+76
-61
index.tsx
web/app/components/share/chat/sidebar/list/index.tsx
+115
-0
style.module.css
web/app/components/share/chat/sidebar/list/style.module.css
+7
-0
share-app.en.ts
web/i18n/lang/share-app.en.ts
+34
-28
share-app.zh.ts
web/i18n/lang/share-app.zh.ts
+34
-28
share.ts
web/service/share.ts
+19
-5
No files found.
web/app/components/base/confirm-ui/index.tsx
View file @
ec261aea
...
...
@@ -24,7 +24,7 @@ const ConfirmUI: FC<IConfirmUIProps> = ({
})
=>
{
const
{
t
}
=
useTranslation
()
return
(
<
div
className=
"w-[420px] rounded-lg p-7 bg-white"
>
<
div
className=
"w-[420px]
max-w-full
rounded-lg p-7 bg-white"
>
<
div
className=
'flex items-center'
>
{
type
===
'info'
&&
(<
svg
width=
"32"
height=
"32"
viewBox=
"0 0 32 32"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
path
d=
"M17.3333 21.3333H16V16H14.6667M16 10.6667H16.0133M28 16C28 17.5759 27.6896 19.1363 27.0866 20.5922C26.4835 22.0481 25.5996 23.371 24.4853 24.4853C23.371 25.5996 22.0481 26.4835 20.5922 27.0866C19.1363 27.6896 17.5759 28 16 28C14.4241 28 12.8637 27.6896 11.4078 27.0866C9.95189 26.4835 8.62902 25.5996 7.51472 24.4853C6.40042 23.371 5.5165 22.0481 4.91345 20.5922C4.31039 19.1363 4 17.5759 4 16C4 12.8174 5.26428 9.76516 7.51472 7.51472C9.76516 5.26428 12.8174 4 16 4C19.1826 4 22.2348 5.26428 24.4853 7.51472C26.7357 9.76516 28 12.8174 28 16Z"
stroke=
"#9CA3AF"
strokeWidth=
"2"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
...
...
web/app/components/explore/item-operation/index.tsx
View file @
ec261aea
...
...
@@ -9,7 +9,7 @@ import s from './style.module.css'
import
Popover
from
'@/app/components/base/popover'
const
PinIcon
=
(
<
svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
svg
className=
"shrink-0"
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
path
d=
"M8.00012 9.99967L8.00012 14.6663M5.33346 4.87176V6.29217C5.33346 6.43085 5.33346 6.50019 5.31985 6.56652C5.30777 6.62536 5.2878 6.6823 5.26047 6.73579C5.22966 6.79608 5.18635 6.85023 5.09972 6.95852L4.0532 8.26667C3.60937 8.82145 3.38746 9.09884 3.38721 9.33229C3.38699 9.53532 3.4793 9.72738 3.63797 9.85404C3.82042 9.99967 4.17566 9.99967 4.88612 9.99967H11.1141C11.8246 9.99967 12.1798 9.99967 12.3623 9.85404C12.5209 9.72738 12.6133 9.53532 12.613 9.33229C12.6128 9.09884 12.3909 8.82145 11.947 8.26667L10.9005 6.95852C10.8139 6.85023 10.7706 6.79608 10.7398 6.73579C10.7125 6.6823 10.6925 6.62536 10.6804 6.56652C10.6668 6.50019 10.6668 6.43085 10.6668 6.29217V4.87176C10.6668 4.79501 10.6668 4.75664 10.6711 4.71879C10.675 4.68517 10.6814 4.6519 10.6903 4.61925C10.7003 4.5825 10.7146 4.54687 10.7431 4.47561L11.415 2.79582C11.611 2.30577 11.709 2.06074 11.6682 1.86404C11.6324 1.69203 11.5302 1.54108 11.3838 1.44401C11.2163 1.33301 10.9524 1.33301 10.4246 1.33301H5.57563C5.04782 1.33301 4.78391 1.33301 4.61646 1.44401C4.47003 1.54108 4.36783 1.69203 4.33209 1.86404C4.29122 2.06074 4.38923 2.30577 4.58525 2.79583L5.25717 4.47561C5.28567 4.54687 5.29992 4.5825 5.30995 4.61925C5.31886 4.6519 5.32526 4.68517 5.32912 4.71879C5.33346 4.75664 5.33346 4.79501 5.33346 4.87176Z"
stroke=
"#667085"
strokeWidth=
"1.5"
strokeLinecap=
"round"
strokeLinejoin=
"round"
/>
</
svg
>
)
...
...
@@ -43,7 +43,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
</
div
>
{
isShowDelete
&&
(
<
div
className=
{
cn
(
s
.
actionItem
,
s
.
deleteActionItem
,
'hover:bg-gray-50 group'
)
}
onClick=
{
onDelete
}
>
<
TrashIcon
className=
{
'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'
}
/>
<
TrashIcon
className=
{
'
shrink-0
w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'
}
/>
<
span
className=
{
cn
(
s
.
actionName
,
'group-hover:text-red-500'
)
}
>
{
t
(
'explore.sidebar.action.delete'
)
}
</
span
>
</
div
>
)
}
...
...
web/app/components/share/chat/hooks/use-conversation.ts
View file @
ec261aea
import
{
useState
}
from
'react'
import
type
{
ConversationItem
}
from
'@/models/share'
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
=
''
)
=>
{
...
...
@@ -29,9 +30,10 @@ function useConversation() {
// 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
=>
{
if
(
!
newConversationInputs
)
return
setNewConversationInputs
(
produce
(
newConversationInputs
,
(
draft
)
=>
{
Object
.
keys
(
draft
).
forEach
((
key
)
=>
{
draft
[
key
]
=
''
})
}))
...
...
@@ -48,6 +50,8 @@ function useConversation() {
return
{
conversationList
,
setConversationList
,
pinnedConversationList
,
setPinnedConversationList
,
currConversationId
,
setCurrConversationId
,
getConversationIdFromStorage
,
...
...
@@ -59,8 +63,8 @@ function useConversation() {
setCurrInputs
,
currConversationInfo
,
setNewConversationInfo
,
setExistConversationInfo
setExistConversationInfo
,
}
}
export
default
useConversation
;
\ No newline at end of file
export
default
useConversation
web/app/components/share/chat/index.tsx
View file @
ec261aea
...
...
@@ -14,7 +14,7 @@ import { ToastContext } from '@/app/components/base/toast'
import
Sidebar
from
'@/app/components/share/chat/sidebar'
import
ConfigSence
from
'@/app/components/share/chat/config-scence'
import
Header
from
'@/app/components/share/header'
import
{
fetchAppInfo
,
fetchAppParams
,
fetchChatList
,
fetchConversations
,
fetchSuggestedQuestions
,
sendChatMessage
,
stopChatMessageResponding
,
updateFeedback
}
from
'@/service/share'
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'
...
...
@@ -25,6 +25,7 @@ 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
...
...
@@ -65,9 +66,12 @@ const Main: FC<IMainProps> = ({
/*
* conversation info
*/
const
[
allConversationList
,
setAllConversationList
]
=
useState
<
ConversationItem
[]
>
([])
const
{
conversationList
,
setConversationList
,
pinnedConversationList
,
setPinnedConversationList
,
currConversationId
,
setCurrConversationId
,
getConversationIdFromStorage
,
...
...
@@ -81,11 +85,50 @@ const Main: FC<IMainProps> = ({
setNewConversationInfo
,
setExistConversationInfo
,
}
=
useConversation
()
const
[
hasMore
,
setHasMore
]
=
useState
<
boolean
>
(
false
)
const
[
hasMore
,
setHasMore
]
=
useState
<
boolean
>
(
true
)
const
[
hasPinnedMore
,
setHasPinnedMore
]
=
useState
<
boolean
>
(
true
)
const
onMoreLoaded
=
({
data
:
conversations
,
has_more
}:
any
)
=>
{
setHasMore
(
has_more
)
setConversationList
([...
conversationList
,
...
conversations
])
}
const
onPinnedMoreLoaded
=
({
data
:
conversations
,
has_more
}:
any
)
=>
{
setHasPinnedMore
(
has_more
)
setPinnedConversationList
([...
pinnedConversationList
,
...
conversations
])
}
const
[
controlUpdateConversationList
,
setControlUpdateConversationList
]
=
useState
(
0
)
const
noticeUpdateList
=
()
=>
{
setConversationList
([])
setHasMore
(
true
)
setPinnedConversationList
([])
setHasPinnedMore
(
true
)
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
()
noticeUpdateList
()
}
const
[
suggestedQuestionsAfterAnswerConfig
,
setSuggestedQuestionsAfterAnswerConfig
]
=
useState
<
SuggestedQuestionsAfterAnswerConfig
|
null
>
(
null
)
const
[
conversationIdChangeBecauseOfNew
,
setConversationIdChangeBecauseOfNew
,
getConversationIdChangeBecauseOfNew
]
=
useGetState
(
false
)
...
...
@@ -121,7 +164,7 @@ const Main: FC<IMainProps> = ({
let
notSyncToStateIntroduction
=
''
let
notSyncToStateInputs
:
Record
<
string
,
any
>
|
undefined
|
null
=
{}
if
(
!
isNewConversation
)
{
const
item
=
c
onversationList
.
find
(
item
=>
item
.
id
===
currConversationId
)
const
item
=
allC
onversationList
.
find
(
item
=>
item
.
id
===
currConversationId
)
notSyncToStateInputs
=
item
?.
inputs
||
{}
setCurrInputs
(
notSyncToStateInputs
)
notSyncToStateIntroduction
=
item
?.
introduction
||
''
...
...
@@ -229,6 +272,10 @@ const Main: FC<IMainProps> = ({
return
[]
}
const
fetchAllConversations
=
()
=>
{
return
fetchConversations
(
isInstalledApp
,
installedAppInfo
?.
id
,
undefined
,
undefined
,
100
)
}
const
fetchInitData
=
()
=>
{
return
Promise
.
all
([
isInstalledApp
?
{
...
...
@@ -240,7 +287,7 @@ const Main: FC<IMainProps> = ({
},
plan
:
'basic'
,
}
:
fetchAppInfo
(),
fetch
Conversations
(
isInstalledApp
,
installedAppInfo
?.
id
),
fetchAppParams
(
isInstalledApp
,
installedAppInfo
?.
id
)])
:
fetchAppInfo
(),
fetch
AllConversations
(
),
fetchAppParams
(
isInstalledApp
,
installedAppInfo
?.
id
)])
}
// init
...
...
@@ -255,10 +302,10 @@ const Main: FC<IMainProps> = ({
setIsPublicVersion
(
tempIsPublicVersion
)
const
prompt_template
=
''
// handle current conversation id
const
{
data
:
conversations
,
has_more
}
=
conversationData
as
{
data
:
ConversationItem
[];
has_more
:
boolean
}
const
{
data
:
allConversations
}
=
conversationData
as
{
data
:
ConversationItem
[];
has_more
:
boolean
}
const
_conversationId
=
getConversationIdFromStorage
(
appId
)
const
isNotNewConversation
=
c
onversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
set
HasMore
(
has_more
)
const
isNotNewConversation
=
allC
onversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
set
AllConversationList
(
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
)
...
...
@@ -276,7 +323,7 @@ const Main: FC<IMainProps> = ({
}
as
PromptConfig
)
setSuggestedQuestionsAfterAnswerConfig
(
suggested_questions_after_answer
)
setConversationList
(
conversations
as
ConversationItem
[])
//
setConversationList(conversations as ConversationItem[])
if
(
isNotNewConversation
)
setCurrConversationId
(
_conversationId
,
appId
,
false
)
...
...
@@ -403,12 +450,10 @@ const Main: FC<IMainProps> = ({
if
(
hasError
)
return
let
currChatList
=
conversationList
if
(
getConversationIdChangeBecauseOfNew
())
{
const
{
data
:
conversations
,
has_more
}:
any
=
await
fetchConversations
(
isInstalledApp
,
installedAppInfo
?.
id
)
setHasMore
(
has_more
)
setConversationList
(
conversations
as
ConversationItem
[])
currChatList
=
conversations
const
{
data
:
allConversations
}:
any
=
await
fetchAllConversations
()
setAllConversationList
(
allConversations
)
noticeUpdateList
()
}
setConversationIdChangeBecauseOfNew
(
false
)
resetNewConversationInputs
()
...
...
@@ -451,14 +496,21 @@ const Main: FC<IMainProps> = ({
return
(
<
Sidebar
list=
{
conversationList
}
pinnedList=
{
pinnedConversationList
}
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
}
/>
)
}
...
...
@@ -482,9 +534,6 @@ const Main: FC<IMainProps> = ({
/>
)
}
{
/* {isNewConversation ? 'new' : 'exist'}
{JSON.stringify(newConversationInputs ? newConversationInputs : {})}
{JSON.stringify(existConversationInputs ? existConversationInputs : {})} */
}
<
div
className=
{
cn
(
'flex rounded-t-2xl bg-white overflow-hidden'
,
...
...
@@ -551,6 +600,17 @@ const Main: FC<IMainProps> = ({
</
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
>
...
...
web/app/components/share/chat/sidebar/index.tsx
View file @
ec261aea
import
React
,
{
use
Ref
}
from
'react'
import
React
,
{
use
Effect
,
useState
}
from
'react'
import
type
{
FC
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
ChatBubbleOvalLeftEllipsisIcon
,
PencilSquareIcon
,
}
from
'@heroicons/react/24/outline'
import
{
ChatBubbleOvalLeftEllipsisIcon
as
ChatBubbleOvalLeftEllipsisSolidIcon
}
from
'@heroicons/react/24/solid'
import
{
useInfiniteScroll
}
from
'ahooks'
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'
function
classNames
(...
classes
:
any
[])
{
return
classes
.
filter
(
Boolean
).
join
(
' '
)
}
export
type
ISidebarProps
=
{
copyRight
:
string
currentId
:
string
onCurrentIdChange
:
(
id
:
string
)
=>
void
list
:
ConversationItem
[]
pinnedList
:
ConversationItem
[]
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
>
=
({
...
...
@@ -34,37 +36,42 @@ const Sidebar: FC<ISidebarProps> = ({
currentId
,
onCurrentIdChange
,
list
,
pinnedList
,
isInstalledApp
,
installedAppId
,
siteInfo
,
onMoreLoaded
,
onPinnedMoreLoaded
,
isNoMore
,
isPinnedNoMore
,
onPin
,
onUnpin
,
controlUpdateList
,
onDelete
,
})
=>
{
const
{
t
}
=
useTranslation
()
const
listRef
=
useRef
<
HTMLDivElement
>
(
null
)
const
[
hasPinned
,
setHasPinned
]
=
useState
(
false
)
useInfiniteScroll
(
async
()
=>
{
if
(
!
isNoMore
)
{
const
lastId
=
list
[
list
.
length
-
1
].
id
const
{
data
:
conversations
,
has_more
}:
any
=
await
fetchConversations
(
isInstalledApp
,
installedAppId
,
lastId
)
onMoreLoaded
({
data
:
conversations
,
has_more
})
}
return
{
list
:
[]
}
},
{
target
:
listRef
,
isNoMore
:
()
=>
{
return
isNoMore
},
reloadDeps
:
[
isNoMore
],
},
)
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=
{
c
lassNames
(
c
n
(
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'
,
)
...
...
@@ -85,40 +92,48 @@ const Sidebar: FC<ISidebarProps> = ({
<
PencilSquareIcon
className=
"mr-2 h-4 w-4"
/>
{
t
(
'share.chat.newChat'
)
}
</
Button
>
</
div
>
<
nav
ref=
{
listRef
}
className=
"mt-4 flex-1 space-y-1 bg-white p-4 !pt-0 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=
{
classNames
(
isCurrent
?
'bg-primary-50 text-primary-600'
:
'text-gray-700 hover:bg-gray-100 hover:text-gray-700'
,
'group flex items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer'
,
)
}
>
<
ItemIcon
className=
{
classNames
(
isCurrent
?
'text-primary-600'
:
'text-gray-400 group-hover:text-gray-500'
,
'mr-3 h-5 w-5 flex-shrink-0'
,
)
}
aria
-
hidden=
"true"
/>
{
item
.
name
}
</
div
>
)
})
}
</
nav
>
<
div
className=
'flex-grow h-0 overflow-y-auto overflow-x-hidden'
>
{
/* pinned list */
}
{
hasPinned
&&
(
<
div
className=
'mt-4 px-4'
>
<
div
className=
'mb-1.5 leading-[18px] text-xs text-gray-500 font-medium uppercase'
>
{
t
(
'share.chat.pinnedTitle'
)
}
</
div
>
<
List
className=
{
maxListHeight
}
currentId=
{
currentId
}
onCurrentIdChange=
{
onCurrentIdChange
}
list=
{
pinnedList
}
isInstalledApp=
{
isInstalledApp
}
installedAppId=
{
installedAppId
}
onMoreLoaded=
{
onPinnedMoreLoaded
}
isNoMore=
{
isPinnedNoMore
}
isPinned=
{
true
}
onPinChanged=
{
id
=>
onUnpin
(
id
)
}
controlUpdate=
{
controlUpdateList
+
1
}
onDelete=
{
onDelete
}
/>
</
div
>
)
}
{
/* unpinned list */
}
<
div
className=
'mt-4 px-4'
>
{
hasPinned
&&
(
<
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
}
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
>
...
...
web/app/components/share/chat/sidebar/list/index.tsx
0 → 100644
View file @
ec261aea
'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
[]
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
,
isInstalledApp
,
installedAppId
,
onMoreLoaded
,
isNoMore
,
isPinned
,
onPinChanged
,
controlUpdate
,
onDelete
,
})
=>
{
const
listRef
=
useRef
<
HTMLDivElement
>
(
null
)
useInfiniteScroll
(
async
()
=>
{
if
(
!
isNoMore
)
{
const
lastId
=
list
[
list
.
length
-
1
]?.
id
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-[60px] 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
>
{
!
isCurrent
&&
(
<
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/chat/sidebar/list/style.module.css
0 → 100644
View file @
ec261aea
.opBtn
{
visibility
:
hidden
;
}
.item
:hover
.opBtn
{
visibility
:
visible
;
}
\ No newline at end of file
web/i18n/lang/share-app.en.ts
View file @
ec261aea
const
translation
=
{
common
:
{
welcome
:
"Welcome to use"
,
appUnavailable
:
"App is unavailable"
,
appUnkonwError
:
"App is unavailable"
welcome
:
'Welcome to use'
,
appUnavailable
:
'App is unavailable'
,
appUnkonwError
:
'App is unavailable'
,
},
chat
:
{
newChat
:
"New chat"
,
newChatDefaultName
:
"New conversation"
,
powerBy
:
"Powered by"
,
prompt
:
"Prompt"
,
privatePromptConfigTitle
:
"Conversation settings"
,
publicPromptConfigTitle
:
"Initial Prompt"
,
configStatusDes
:
"Before start, you can modify conversation settings"
,
newChat
:
'New chat'
,
pinnedTitle
:
'Pinned'
,
unpinnedTitle
:
'Chats'
,
newChatDefaultName
:
'New conversation'
,
powerBy
:
'Powered by'
,
prompt
:
'Prompt'
,
privatePromptConfigTitle
:
'Conversation settings'
,
publicPromptConfigTitle
:
'Initial Prompt'
,
configStatusDes
:
'Before start, you can modify conversation settings'
,
configDisabled
:
"Previous session settings have been used for this session."
,
startChat
:
"Start Chat"
,
'Previous session settings have been used for this session.'
,
startChat
:
'Start Chat'
,
privacyPolicyLeft
:
"Please read the "
,
'Please read the '
,
privacyPolicyMiddle
:
"privacy policy"
,
'privacy policy'
,
privacyPolicyRight
:
" provided by the app developer."
,
' provided by the app developer.'
,
deleteConversation
:
{
title
:
'Delete conversation'
,
content
:
'Are you sure you want to delete this conversation?'
,
},
},
generation
:
{
tabs
:
{
create
:
"Create"
,
saved
:
"Saved"
,
create
:
'Create'
,
saved
:
'Saved'
,
},
savedNoData
:
{
title
:
"You haven't saved a result yet!"
,
title
:
'You haven
\'
t saved a result yet!'
,
description
:
'Start generating content, and find your saved results here.'
,
startCreateContent
:
'Start create content'
startCreateContent
:
'Start create content'
,
},
title
:
"AI Completion"
,
queryTitle
:
"Query content"
,
queryPlaceholder
:
"Write your query content..."
,
run
:
"RUN"
,
copy
:
"Copy"
,
resultTitle
:
"AI Completion"
,
noData
:
"AI will give you what you want here."
,
title
:
'AI Completion'
,
queryTitle
:
'Query content'
,
queryPlaceholder
:
'Write your query content...'
,
run
:
'RUN'
,
copy
:
'Copy'
,
resultTitle
:
'AI Completion'
,
noData
:
'AI will give you what you want here.'
,
},
}
;
}
export
default
translation
;
export
default
translation
web/i18n/lang/share-app.zh.ts
View file @
ec261aea
const
translation
=
{
common
:
{
welcome
:
"欢迎使用"
,
appUnavailable
:
"应用不可用"
,
appUnkonwError
:
"应用不可用"
,
welcome
:
'欢迎使用'
,
appUnavailable
:
'应用不可用'
,
appUnkonwError
:
'应用不可用'
,
},
chat
:
{
newChat
:
"新对话"
,
newChatDefaultName
:
"新的对话"
,
powerBy
:
"Powered by"
,
prompt
:
"提示词"
,
privatePromptConfigTitle
:
"对话设置"
,
publicPromptConfigTitle
:
"对话前提示词"
,
configStatusDes
:
"开始前,您可以修改对话设置"
,
configDisabled
:
"此次会话已使用上次会话表单"
,
startChat
:
"开始对话"
,
privacyPolicyLeft
:
"请阅读由该应用开发者提供的"
,
privacyPolicyMiddle
:
"隐私政策"
,
privacyPolicyRight
:
"。"
,
newChat
:
'新对话'
,
pinnedTitle
:
'已置顶'
,
unpinnedTitle
:
'对话列表'
,
newChatDefaultName
:
'新的对话'
,
powerBy
:
'Powered by'
,
prompt
:
'提示词'
,
privatePromptConfigTitle
:
'对话设置'
,
publicPromptConfigTitle
:
'对话前提示词'
,
configStatusDes
:
'开始前,您可以修改对话设置'
,
configDisabled
:
'此次会话已使用上次会话表单'
,
startChat
:
'开始对话'
,
privacyPolicyLeft
:
'请阅读由该应用开发者提供的'
,
privacyPolicyMiddle
:
'隐私政策'
,
privacyPolicyRight
:
'。'
,
deleteConversation
:
{
title
:
'删除对话'
,
content
:
'您确定要删除此对话吗?'
,
},
},
generation
:
{
tabs
:
{
create
:
"创建"
,
saved
:
"已保存"
,
create
:
'创建'
,
saved
:
'已保存'
,
},
savedNoData
:
{
title
:
"您还没有保存结果!"
,
title
:
'您还没有保存结果!'
,
description
:
'开始生成内容,您可以在这里找到保存的结果。'
,
startCreateContent
:
'开始生成内容'
startCreateContent
:
'开始生成内容'
,
},
title
:
"AI 智能书写"
,
queryTitle
:
"查询内容"
,
queryPlaceholder
:
"请输入文本内容"
,
run
:
"运行"
,
copy
:
"拷贝"
,
resultTitle
:
"AI 书写"
,
noData
:
"AI 会在这里给你惊喜。"
,
title
:
'AI 智能书写'
,
queryTitle
:
'查询内容'
,
queryPlaceholder
:
'请输入文本内容'
,
run
:
'运行'
,
copy
:
'拷贝'
,
resultTitle
:
'AI 书写'
,
noData
:
'AI 会在这里给你惊喜。'
,
},
}
;
}
export
default
translation
;
export
default
translation
web/service/share.ts
View file @
ec261aea
import
type
{
IOnCompleted
,
IOnData
,
IOnError
}
from
'./base'
import
{
del
as
consoleDel
,
get
as
consoleGet
,
post
as
consolePost
,
delPublic
as
del
,
getPublic
as
get
,
postPublic
as
post
,
ssePost
,
del
as
consoleDel
,
get
as
consoleGet
,
p
atch
as
consolePatch
,
p
ost
as
consolePost
,
delPublic
as
del
,
getPublic
as
get
,
p
atchPublic
as
patch
,
p
ostPublic
as
post
,
ssePost
,
}
from
'./base'
import
type
{
Feedbacktype
}
from
'@/app/components/app/chat'
function
getAction
(
action
:
'get'
|
'post'
|
'del'
,
isInstalledApp
:
boolean
)
{
function
getAction
(
action
:
'get'
|
'post'
|
'del'
|
'patch'
,
isInstalledApp
:
boolean
)
{
switch
(
action
)
{
case
'get'
:
return
isInstalledApp
?
consoleGet
:
get
case
'post'
:
return
isInstalledApp
?
consolePost
:
post
case
'patch'
:
return
isInstalledApp
?
consolePatch
:
patch
case
'del'
:
return
isInstalledApp
?
consoleDel
:
del
}
...
...
@@ -55,8 +57,20 @@ export const fetchAppInfo = async () => {
return
get
(
'/site'
)
}
export
const
fetchConversations
=
async
(
isInstalledApp
:
boolean
,
installedAppId
=
''
,
last_id
?:
string
)
=>
{
return
getAction
(
'get'
,
isInstalledApp
)(
getUrl
(
'conversations'
,
isInstalledApp
,
installedAppId
),
{
params
:
{
...{
limit
:
20
},
...(
last_id
?
{
last_id
}
:
{})
}
})
export
const
fetchConversations
=
async
(
isInstalledApp
:
boolean
,
installedAppId
=
''
,
last_id
?:
string
,
pinned
?:
boolean
,
limit
?:
number
)
=>
{
return
getAction
(
'get'
,
isInstalledApp
)(
getUrl
(
'conversations'
,
isInstalledApp
,
installedAppId
),
{
params
:
{
...{
limit
:
limit
||
20
},
...(
last_id
?
{
last_id
}
:
{}),
...(
pinned
!==
undefined
?
{
pinned
}
:
{})
}
})
}
export
const
pinConversation
=
async
(
isInstalledApp
:
boolean
,
installedAppId
=
''
,
id
:
string
)
=>
{
return
getAction
(
'patch'
,
isInstalledApp
)(
getUrl
(
`conversations/
${
id
}
/pin`
,
isInstalledApp
,
installedAppId
))
}
export
const
unpinConversation
=
async
(
isInstalledApp
:
boolean
,
installedAppId
=
''
,
id
:
string
)
=>
{
return
getAction
(
'patch'
,
isInstalledApp
)(
getUrl
(
`conversations/
${
id
}
/unpin`
,
isInstalledApp
,
installedAppId
))
}
export
const
delConversation
=
async
(
isInstalledApp
:
boolean
,
installedAppId
=
''
,
id
:
string
)
=>
{
return
getAction
(
'del'
,
isInstalledApp
)(
getUrl
(
`conversations/
${
id
}
`
,
isInstalledApp
,
installedAppId
))
}
export
const
fetchChatList
=
async
(
conversationId
:
string
,
isInstalledApp
:
boolean
,
installedAppId
=
''
)
=>
{
...
...
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