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
04abd4b5
Commit
04abd4b5
authored
Jun 27, 2023
by
Joel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: converation pin and unpin almost done
parent
17d19612
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
253 additions
and
80 deletions
+253
-80
use-conversation.ts
web/app/components/share/chat/hooks/use-conversation.ts
+10
-6
index.tsx
web/app/components/share/chat/index.tsx
+38
-8
index.tsx
web/app/components/share/chat/sidebar/index.tsx
+70
-61
index.tsx
web/app/components/share/chat/sidebar/list/index.tsx
+112
-0
style.module.css
web/app/components/share/chat/sidebar/list/style.module.css
+7
-0
share.ts
web/service/share.ts
+16
-5
No files found.
web/app/components/share/chat/hooks/use-conversation.ts
View file @
04abd4b5
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 @
04abd4b5
...
...
@@ -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
{
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'
...
...
@@ -68,6 +68,8 @@ const Main: FC<IMainProps> = ({
const
{
conversationList
,
setConversationList
,
pinnedConversationList
,
setPinnedConversationList
,
currConversationId
,
setCurrConversationId
,
getConversationIdFromStorage
,
...
...
@@ -81,11 +83,34 @@ 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
)
noticeUpdateList
()
}
const
handleUnpin
=
async
(
id
:
string
)
=>
{
await
unpinConversation
(
isInstalledApp
,
installedAppInfo
?.
id
,
id
)
noticeUpdateList
()
}
const
[
suggestedQuestionsAfterAnswerConfig
,
setSuggestedQuestionsAfterAnswerConfig
]
=
useState
<
SuggestedQuestionsAfterAnswerConfig
|
null
>
(
null
)
const
[
conversationIdChangeBecauseOfNew
,
setConversationIdChangeBecauseOfNew
,
getConversationIdChangeBecauseOfNew
]
=
useGetState
(
false
)
...
...
@@ -258,7 +283,7 @@ const Main: FC<IMainProps> = ({
const
{
data
:
conversations
,
has_more
}
=
conversationData
as
{
data
:
ConversationItem
[];
has_more
:
boolean
}
const
_conversationId
=
getConversationIdFromStorage
(
appId
)
const
isNotNewConversation
=
conversations
.
some
(
item
=>
item
.
id
===
_conversationId
)
setHasMore
(
has_more
)
//
setHasMore(has_more)
// 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 +301,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 +428,11 @@ 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
//
setHasMore(has_more)
//
setConversationList(conversations as ConversationItem[])
setControlUpdateConversationList
(
Date
.
now
())
}
setConversationIdChangeBecauseOfNew
(
false
)
resetNewConversationInputs
()
...
...
@@ -451,14 +475,20 @@ 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
}
/>
)
}
...
...
web/app/components/share/chat/sidebar/index.tsx
View file @
04abd4b5
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
}
const
Sidebar
:
FC
<
ISidebarProps
>
=
({
...
...
@@ -34,37 +35,39 @@ const Sidebar: FC<ISidebarProps> = ({
currentId
,
onCurrentIdChange
,
list
,
pinnedList
,
isInstalledApp
,
installedAppId
,
siteInfo
,
onMoreLoaded
,
onPinnedMoreLoaded
,
isNoMore
,
isPinnedNoMore
,
onPin
,
onUnpin
,
controlUpdateList
,
})
=>
{
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
})
const
checkHasPinned
=
async
()
=>
{
const
{
data
}:
any
=
await
fetchConversations
(
isInstalledApp
,
installedAppId
,
undefined
,
true
)
setHasPinned
(
data
.
length
>
0
)
}
return
{
list
:
[]
}
},
{
target
:
listRef
,
isNoMore
:
()
=>
{
return
isNoMore
},
reloadDeps
:
[
isNoMore
],
},
)
useEffect
(()
=>
{
checkHasPinned
()
},
[])
useEffect
(()
=>
{
if
(
controlUpdateList
!==
0
)
checkHasPinned
()
},
[
controlUpdateList
])
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 +88,46 @@ 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'
,
<
div
className=
'flex-grow'
>
{
/* pinned list */
}
{
hasPinned
&&
(
<
div
className=
'mt-4 px-4'
>
<
div
className=
'leading-[18px] text-xs text-gray-500 font-medium uppercase'
>
Pinned
</
div
>
<
List
className=
'max-h-[40vh]'
currentId=
{
currentId
}
onCurrentIdChange=
{
onCurrentIdChange
}
list=
{
pinnedList
}
isInstalledApp=
{
isInstalledApp
}
installedAppId=
{
installedAppId
}
onMoreLoaded=
{
onPinnedMoreLoaded
}
isNoMore=
{
isPinnedNoMore
}
isPinned=
{
true
}
onPinChanged=
{
id
=>
onUnpin
(
id
)
}
controlUpdate=
{
controlUpdateList
+
1
}
/>
</
div
>
)
}
>
<
ItemIcon
className=
{
classNames
(
isCurrent
?
'text-primary-600'
:
'text-gray-400 group-hover:text-gray-500'
,
'mr-3 h-5 w-5 flex-shrink-0'
,
{
/* unpinned list */
}
<
div
className=
'mt-4 px-4'
>
{
hasPinned
&&
(
<
div
className=
'leading-[18px] text-xs text-gray-500 font-medium uppercase'
>
Chats
</
div
>
)
}
aria
-
hidden=
"true"
<
List
className=
{
cn
(
hasPinned
?
'max-h-[40vh]'
:
'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
}
/>
{
item
.
name
}
</
div
>
)
})
}
</
nav
>
</
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 @
04abd4b5
'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
}
const
List
:
FC
<
IListProps
>
=
({
currentId
,
onCurrentIdChange
,
list
,
isInstalledApp
,
installedAppId
,
onMoreLoaded
,
isNoMore
,
isPinned
,
onPinChanged
,
controlUpdate
,
})
=>
{
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=
"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=
{
false
}
onDelete=
{
()
=>
{}
}
/>
</
div
>
)
}
</
div
>
)
})
}
</
nav
>
)
}
export
default
React
.
memo
(
List
)
web/app/components/share/chat/sidebar/list/style.module.css
0 → 100644
View file @
04abd4b5
.opBtn
{
visibility
:
hidden
;
}
.item
:hover
.opBtn
{
visibility
:
visible
;
}
\ No newline at end of file
web/service/share.ts
View file @
04abd4b5
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,17 @@ 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
)
=>
{
console
.
log
(
pinned
)
return
getAction
(
'get'
,
isInstalledApp
)(
getUrl
(
'conversations'
,
isInstalledApp
,
installedAppId
),
{
params
:
{
...{
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
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