Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
1226c3ef
authored
May 12, 2023
by
archer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: chat ui
parent
39f9080e
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
195 additions
and
104 deletions
+195
-104
public/docs/chatProblem.md
+3
-2
src/components/Layout/index.tsx
+2
-0
src/components/Markdown/index.module.scss
+3
-3
src/components/Markdown/index.tsx
+1
-1
src/pages/chat/index.tsx
+183
-97
src/pages/index.tsx
+3
-1
No files found.
public/docs/chatProblem.md
View file @
1226c3ef
...
@@ -11,5 +11,6 @@ chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部
...
@@ -11,5 +11,6 @@ chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器:
[
FastGpt
](
https://fastgpt.run/
)
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器:
[
FastGpt
](
https://fastgpt.run/
)
**其他问题**
**其他问题**
请 WX 联系: fastgpt123
请 WX 联系: fastgpt123
!
[
](/imgs/wxqun300.jpg)
| 交流群 | 小助手 |
!
[
](/imgs/wx300.jpg)
| ----------------------- | -------------------- |
| !
[
](/imgs/wxqun300.jpg) | 
|
src/components/Layout/index.tsx
View file @
1226c3ef
...
@@ -9,9 +9,11 @@ import Navbar from './navbar';
...
@@ -9,9 +9,11 @@ import Navbar from './navbar';
import
NavbarPhone
from
'./navbarPhone'
;
import
NavbarPhone
from
'./navbarPhone'
;
const
pcUnShowLayoutRoute
:
Record
<
string
,
boolean
>
=
{
const
pcUnShowLayoutRoute
:
Record
<
string
,
boolean
>
=
{
'/'
:
true
,
'/login'
:
true
'/login'
:
true
};
};
const
phoneUnShowLayoutRoute
:
Record
<
string
,
boolean
>
=
{
const
phoneUnShowLayoutRoute
:
Record
<
string
,
boolean
>
=
{
'/'
:
true
,
'/login'
:
true
'/login'
:
true
};
};
...
...
src/components/Markdown/index.module.scss
View file @
1226c3ef
...
@@ -337,10 +337,10 @@
...
@@ -337,10 +337,10 @@
}
}
.markdown
{
.markdown
{
text-align
:
justify
;
text-align
:
justify
;
overflow-y
:
hidden
;
tab-size
:
4
;
tab-size
:
4
;
word-spacing
:
normal
;
word-spacing
:
normal
;
word-break
:
break-all
;
word-break
:
break-all
;
width
:
100%
;
p
{
p
{
white-space
:
pre-line
;
white-space
:
pre-line
;
...
@@ -353,13 +353,13 @@
...
@@ -353,13 +353,13 @@
margin
:
0
;
margin
:
0
;
border
:
none
;
border
:
none
;
border-radius
:
0
;
border-radius
:
0
;
background-color
:
#2
22
!
important
;
background-color
:
#2
92b33
!
important
;
overflow-x
:
auto
;
overflow-x
:
auto
;
color
:
#fff
;
color
:
#fff
;
}
}
pre
code
{
pre
code
{
background-color
:
#2
22
!
important
;
background-color
:
#2
92b33
!
important
;
width
:
100%
;
width
:
100%
;
}
}
...
...
src/components/Markdown/index.tsx
View file @
1226c3ef
...
@@ -29,7 +29,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
...
@@ -29,7 +29,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
const
code
=
String
(
children
);
const
code
=
String
(
children
);
return
!
inline
||
match
?
(
return
!
inline
||
match
?
(
<
Box
my=
{
3
}
borderRadius=
{
'md'
}
overflow=
{
'
hidden
'
}
backgroundColor=
{
'#222'
}
>
<
Box
my=
{
3
}
borderRadius=
{
'md'
}
overflow=
{
'
overlay
'
}
backgroundColor=
{
'#222'
}
>
<
Flex
<
Flex
className=
"code-header"
className=
"code-header"
py=
{
2
}
py=
{
2
}
...
...
src/pages/chat/index.tsx
View file @
1226c3ef
import
React
,
{
useCallback
,
useState
,
useRef
,
useMemo
,
useEffect
}
from
'react'
;
import
React
,
{
useCallback
,
useState
,
useRef
,
useMemo
,
useEffect
,
MouseEvent
}
from
'react'
;
import
{
useRouter
}
from
'next/router'
;
import
{
useRouter
}
from
'next/router'
;
import
{
import
{
getInitChatSiteInfo
,
getInitChatSiteInfo
,
...
@@ -26,7 +26,10 @@ import {
...
@@ -26,7 +26,10 @@ import {
useDisclosure
,
useDisclosure
,
Drawer
,
Drawer
,
DrawerOverlay
,
DrawerOverlay
,
DrawerContent
DrawerContent
,
Card
,
Tooltip
,
useOutsideClick
}
from
'@chakra-ui/react'
;
}
from
'@chakra-ui/react'
;
import
{
useToast
}
from
'@/hooks/useToast'
;
import
{
useToast
}
from
'@/hooks/useToast'
;
import
{
useScreen
}
from
'@/hooks/useScreen'
;
import
{
useScreen
}
from
'@/hooks/useScreen'
;
...
@@ -76,6 +79,8 @@ const Chat = ({
...
@@ -76,6 +79,8 @@ const Chat = ({
const
ChatBox
=
useRef
<
HTMLDivElement
>
(
null
);
const
ChatBox
=
useRef
<
HTMLDivElement
>
(
null
);
const
TextareaDom
=
useRef
<
HTMLTextAreaElement
>
(
null
);
const
TextareaDom
=
useRef
<
HTMLTextAreaElement
>
(
null
);
const
ContextMenuRef
=
useRef
(
null
);
const
PhoneContextShow
=
useRef
(
false
);
// 中断请求
// 中断请求
const
controller
=
useRef
(
new
AbortController
());
const
controller
=
useRef
(
new
AbortController
());
...
@@ -83,6 +88,12 @@ const Chat = ({
...
@@ -83,6 +88,12 @@ const Chat = ({
const
[
inputVal
,
setInputVal
]
=
useState
(
''
);
// user input prompt
const
[
inputVal
,
setInputVal
]
=
useState
(
''
);
// user input prompt
const
[
showSystemPrompt
,
setShowSystemPrompt
]
=
useState
(
''
);
const
[
showSystemPrompt
,
setShowSystemPrompt
]
=
useState
(
''
);
const
[
messageContextMenuData
,
setMessageContextMenuData
]
=
useState
<
{
// message messageContextMenuData
left
:
number
;
top
:
number
;
message
:
ChatSiteItemType
;
}
>
();
const
{
const
{
lastChatModelId
,
lastChatModelId
,
...
@@ -107,6 +118,24 @@ const Chat = ({
...
@@ -107,6 +118,24 @@ const Chat = ({
const
{
Loading
,
setIsLoading
}
=
useLoading
();
const
{
Loading
,
setIsLoading
}
=
useLoading
();
const
{
userInfo
}
=
useUserStore
();
const
{
userInfo
}
=
useUserStore
();
const
{
isOpen
:
isOpenSlider
,
onClose
:
onCloseSlider
,
onOpen
:
onOpenSlider
}
=
useDisclosure
();
const
{
isOpen
:
isOpenSlider
,
onClose
:
onCloseSlider
,
onOpen
:
onOpenSlider
}
=
useDisclosure
();
// close contextMenu
useOutsideClick
({
ref
:
ContextMenuRef
,
handler
:
()
=>
{
// 移动端长按后会将其设置为true,松手时候也会触发一次,松手的时候需要忽略一次。
if
(
PhoneContextShow
.
current
)
{
PhoneContextShow
.
current
=
false
;
}
else
{
messageContextMenuData
&&
setTimeout
(()
=>
{
setMessageContextMenuData
(
undefined
);
window
.
getSelection
?.()?.
empty
?.();
window
.
getSelection
?.()?.
removeAllRanges
?.();
document
?.
getSelection
()?.
empty
();
});
}
}
});
// 滚动到底部
// 滚动到底部
const
scrollToBottom
=
useCallback
((
behavior
:
'smooth'
|
'auto'
=
'smooth'
)
=>
{
const
scrollToBottom
=
useCallback
((
behavior
:
'smooth'
|
'auto'
=
'smooth'
)
=>
{
...
@@ -234,6 +263,9 @@ const Chat = ({
...
@@ -234,6 +263,9 @@ const Chat = ({
// refresh history
// refresh history
loadHistory
({
pageNum
:
1
,
init
:
true
});
loadHistory
({
pageNum
:
1
,
init
:
true
});
setTimeout
(()
=>
{
generatingMessage
();
},
100
);
},
},
[
[
chatId
,
chatId
,
...
@@ -327,24 +359,26 @@ const Chat = ({
...
@@ -327,24 +359,26 @@ const Chat = ({
]);
]);
// 删除一句话
// 删除一句话
const
delChatRecord
=
useCallback
(
const
delChatRecord
=
useCallback
(
async
()
=>
{
async
(
index
:
number
,
id
:
string
)
=>
{
if
(
!
messageContextMenuData
)
return
;
setIsLoading
(
true
);
setIsLoading
(
true
);
try
{
const
index
=
chatData
.
history
.
findIndex
(
// 删除数据库最后一句
(
item
)
=>
item
.
_id
===
messageContextMenuData
.
message
.
_id
await
delChatRecordByIndex
(
chatId
,
id
);
);
setChatData
((
state
)
=>
({
try
{
...
state
,
// 删除数据库最后一句
history
:
state
.
history
.
filter
((
_
,
i
)
=>
i
!==
index
)
await
delChatRecordByIndex
(
chatId
,
messageContextMenuData
.
message
.
_id
);
}));
}
catch
(
err
)
{
setChatData
((
state
)
=>
({
console
.
log
(
err
);
...
state
,
}
history
:
state
.
history
.
filter
((
_
,
i
)
=>
i
!==
index
)
setIsLoading
(
false
);
}));
},
}
catch
(
err
)
{
[
chatId
,
setChatData
,
setIsLoading
]
console
.
log
(
err
);
);
}
setIsLoading
(
false
);
},
[
chatData
.
history
,
chatId
,
messageContextMenuData
,
setChatData
,
setIsLoading
]);
// 复制内容
// 复制内容
const
onclickCopy
=
useCallback
(
const
onclickCopy
=
useCallback
(
...
@@ -433,6 +467,34 @@ const Chat = ({
...
@@ -433,6 +467,34 @@ const Chat = ({
[loadHistory]
[loadHistory]
);
);
// onclick chat message context
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, message: ChatSiteItemType) => {
e.preventDefault(); // 阻止默认右键菜单
// select all text
const range = document.createRange();
range.selectNodeContents(e.currentTarget as HTMLDivElement);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
navigator.vibrate?.(50); // 震动 50 毫秒
if (!isPcDevice) {
PhoneContextShow.current = true;
}
setMessageContextMenuData({
left: e.clientX,
top: e.clientY,
message
});
return false;
},
[isPcDevice]
);
// 获取对话信息
// 获取对话信息
const loadChatInfo = useCallback(
const loadChatInfo = useCallback(
async ({
async ({
...
@@ -511,6 +573,7 @@ const Chat = ({
...
@@ -511,6 +573,7 @@ const Chat = ({
});
});
});
});
// abort stream
useEffect(() => {
useEffect(() => {
return () => {
return () => {
isLeavePage.current = true;
isLeavePage.current = true;
...
@@ -522,7 +585,7 @@ const Chat = ({
...
@@ -522,7 +585,7 @@ const Chat = ({
<Flex
<Flex
h={'100%'}
h={'100%'}
flexDirection={['column', 'row']}
flexDirection={['column', 'row']}
backgroundColor={useColorModeValue('
whit
e', '')}
backgroundColor={useColorModeValue('
#fefef
e', '')}
>
>
{/* pc always show history. phone is only show when modelId is present */}
{/* pc always show history. phone is only show when modelId is present */}
{isPc || !modelId ? (
{isPc || !modelId ? (
...
@@ -605,6 +668,7 @@ const Chat = ({
...
@@ -605,6 +668,7 @@ const Chat = ({
position={'relative'}
position={'relative'}
h={[0, '100%']}
h={[0, '100%']}
w={['100%', 0]}
w={['100%', 0]}
pt={[2, 4]}
flex={'1 0 0'}
flex={'1 0 0'}
flexDirection={'column'}
flexDirection={'column'}
>
>
...
@@ -617,20 +681,25 @@ const Chat = ({
...
@@ -617,20 +681,25 @@ const Chat = ({
w={'100%'}
w={'100%'}
overflow={'overlay'}
overflow={'overlay'}
>
>
{chatData.history.map((item, index) => (
<Box maxW={['auto', '800px', '1000px']} m={'auto'}>
<Box
{chatData.history.map((item, index) => (
key={item._id}
<Flex key={item._id} alignItems={'flex-start'} py={2} px={[2, 4]}>
py={[6, 9]}
{item.obj === 'Human' && <Box flex={1} />}
px={[2, 4]}
{/* avatar */}
backgroundColor={
<Box
index % 2 !== 0 ? useColorModeValue('blackAlpha.50', 'gray.700') : ''
{...(item.obj === 'AI'
}
? {
color={useColorModeValue('blackAlpha.700', 'white')}
order: 1,
borderBottom={'1px solid rgba(0,0,0,0.1)'}
mr: ['6px', 4],
>
cursor: 'pointer',
<Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
onClick: () => router.push(`/model?modelId=${chatData.modelId}`)
<Menu autoSelect={false}>
}
<MenuButton as={Box} mr={[1, 4]} cursor={'pointer'}>
: {
order: 3,
ml: ['6px', 4]
})}
>
<Tooltip label={item.obj === 'AI' ? 'AI助手详情' : ''}>
<Image
<Image
className="
avatar
"
className="
avatar
"
src={
src={
...
@@ -639,75 +708,73 @@ const Chat = ({
...
@@ -639,75 +708,73 @@ const Chat = ({
: chatData.model.avatar || LOGO_ICON
: chatData.model.avatar || LOGO_ICON
}
}
alt="
avatar
"
alt="
avatar
"
w={['20px', '30px']}
w={['20px', '34px']}
maxH={'50px'}
h={['20px', '34px']}
borderRadius={'50%'}
objectFit={'contain'}
objectFit={'contain'}
/>
/>
</MenuButton>
</Tooltip>
<MenuList fontSize={'sm'}>
</Box>
{chatData.model.canUse && item.obj === 'AI' && (
{/* message */}
<MenuItem onClick={() => router.push(`/model?modelId=${chatData.modelId}`)}>
<Flex order={2} maxW={['calc(100% - 50px)', '80%']}>
AI助手详情
</MenuItem>
)}
<MenuItem onClick={() => onclickCopy(item.value)}>复制</MenuItem>
<MenuItem onClick={() => delChatRecord(index, item._id)}>删除该行</MenuItem>
</MenuList>
</Menu>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
{item.obj === 'AI' ? (
{item.obj === 'AI' ? (
<>
<Box w={'100%'}>
<Markdown
{isPc && (
source={item.value}
<Box color={'myGray.600'} fontSize={'sm'} mb={1}>
isChatting={isChatting && index === chatData.history.length - 1}
{chatData.model.name}
/>
</Box>
{item.systemPrompt && (
<Button
size={'xs'}
mt={2}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'outline'}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
查看提示词
</Button>
)}
)}
</>
<Card
bg={'white'}
px={4}
py={3}
borderRadius={'0 8px 8px 8px'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
/>
{item.systemPrompt && (
<Button
size={'xs'}
mt={2}
fontWeight={'normal'}
colorScheme={'gray'}
variant={'outline'}
w={'90px'}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
查看提示词
</Button>
)}
</Card>
</Box>
) : (
) : (
<Box className="
markdown
" whiteSpace={'pre-wrap'}>
<Box>
<Box as={'p'}>{item.value}</Box>
{isPc && (
<Box color={'myGray.600'} mb={1} fontSize={'sm'} textAlign={'right'}>
Human
</Box>
)}
<Card
className="
markdown
"
whiteSpace={'pre-wrap'}
px={4}
py={3}
borderRadius={'8px 0 8px 8px'}
bg={'myBlue.300'}
onContextMenu={(e) => onclickContextMenu(e, item)}
>
<Box as={'p'}>{item.value}</Box>
</Card>
</Box>
</Box>
)}
)}
</Box>
</Flex>
{/* copy and clear icon */}
{isPc && (
<Flex h={'100%'} flexDirection={'column'} ml={2} w={'14px'} height={'100%'}>
<Box minH={'40px'} flex={1}>
<MyIcon
name="
copy
"
w={'14px'}
cursor={'pointer'}
color={'blackAlpha.700'}
onClick={() => onclickCopy(item.value)}
/>
</Box>
<MyIcon
name="
delete
"
w={'14px'}
cursor={'pointer'}
color={'blackAlpha.700'}
_hover={{
color: 'red.600'
}}
onClick={() => delChatRecord(index, item._id)}
/>
</Flex>
)}
</Flex>
</Flex>
</Box>
))}
))
}
{chatData.history.length === 0 && <Empty model={chatData.model} />
}
{chatData.history.length === 0 && <Empty model={chatData.model} />}
</Box>
</Box>
</Box>
{/* 发送区 */}
{/* 发送区 */}
{chatData.model.canUse ? (
{chatData.model.canUse ? (
...
@@ -715,7 +782,7 @@ const Chat = ({
...
@@ -715,7 +782,7 @@ const Chat = ({
<Box
<Box
py={'18px'}
py={'18px'}
position={'relative'}
position={'relative'}
boxShadow={`0 0 1
5
px rgba(0,0,0,0.1)`}
boxShadow={`0 0 1
0
px rgba(0,0,0,0.1)`}
borderTop={['1px solid', 0]}
borderTop={['1px solid', 0]}
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
borderTopColor={useColorModeValue('gray.200', 'gray.700')}
borderRadius={['none', 'md']}
borderRadius={['none', 'md']}
...
@@ -751,7 +818,7 @@ const Chat = ({
...
@@ -751,7 +818,7 @@ const Chat = ({
}}
}}
onKeyDown
=
{(
e
)
=>
{
onKeyDown
=
{(
e
)
=>
{
// 触发快捷发送
// 触发快捷发送
if
(
isPc
&&
e
.
keyCode
===
13
&&
!
e
.
shiftKey
)
{
if
(
isPc
Device
&&
e
.
keyCode
===
13
&&
!
e
.
shiftKey
)
{
sendPrompt
();
sendPrompt
();
e
.
preventDefault
();
e
.
preventDefault
();
}
}
...
@@ -817,6 +884,25 @@ const Chat = ({
...
@@ -817,6 +884,25 @@ const Chat = ({
</
ModalContent
>
</
ModalContent
>
</
Modal
>
</
Modal
>
}
}
{
/* context menu */
}
{
messageContextMenuData
&&
(
<
Box
zIndex=
{
10
}
position=
{
'fixed'
}
top=
{
messageContextMenuData
.
top
}
left=
{
messageContextMenuData
.
left
}
>
<
Box
ref=
{
ContextMenuRef
}
></
Box
>
<
Menu
isOpen
>
<
MenuList
minW=
{
`100px !important`
}
>
<
MenuItem
onClick=
{
()
=>
onclickCopy
(
messageContextMenuData
.
message
.
value
)
}
>
复制
</
MenuItem
>
<
MenuItem
onClick=
{
delChatRecord
}
>
删除
</
MenuItem
>
</
MenuList
>
</
Menu
>
</
Box
>
)
}
</
Flex
>
</
Flex
>
);
);
};
};
...
...
src/pages/index.tsx
View file @
1226c3ef
...
@@ -141,6 +141,7 @@ const Home = () => {
...
@@ -141,6 +141,7 @@ const Home = () => {
flexDirection=
{
'column'
}
flexDirection=
{
'column'
}
alignItems=
{
'center'
}
alignItems=
{
'center'
}
pt=
{
'20vh'
}
pt=
{
'20vh'
}
overflow=
{
'overlay'
}
>
>
<
Box
id=
{
'particles-js'
}
position=
{
'absolute'
}
top=
{
0
}
left=
{
0
}
right=
{
0
}
bottom=
{
0
}
/>
<
Box
id=
{
'particles-js'
}
position=
{
'absolute'
}
top=
{
0
}
left=
{
0
}
right=
{
0
}
bottom=
{
0
}
/>
<
Image
src=
"/icon/logo.png"
w=
{
[
'70px'
,
'120px'
]
}
h=
{
[
'70px'
,
'120px'
]
}
alt=
{
''
}
></
Image
>
<
Image
src=
"/icon/logo.png"
w=
{
[
'70px'
,
'120px'
]
}
h=
{
[
'70px'
,
'120px'
]
}
alt=
{
''
}
></
Image
>
...
@@ -162,7 +163,8 @@ const Home = () => {
...
@@ -162,7 +163,8 @@ const Home = () => {
<
Button
<
Button
my=
{
5
}
my=
{
5
}
fontSize=
{
[
'xl'
,
'3xl'
]
}
fontSize=
{
[
'xl'
,
'3xl'
]
}
h=
{
[
'35px'
,
'40px'
]
}
h=
{
'auto'
}
py=
{
[
2
,
3
]
}
onClick=
{
()
=>
router
.
push
(
`/model`
)
}
onClick=
{
()
=>
router
.
push
(
`/model`
)
}
>
>
点击开始
点击开始
...
...
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