Merge branch 'new-page-framework' into dev_ty_06_end

This commit is contained in:
tuyang.yhj 2023-06-29 10:28:17 +08:00
commit 16655e5f13
10 changed files with 206 additions and 100 deletions

View File

@ -11,14 +11,27 @@ const AgentPage = (props) => {
ready: !!props.params?.agentId ready: !!props.params?.agentId
}); });
const { handleChatSubmit, history } = useAgentChat({ const { history, handleChatSubmit } = useAgentChat({
queryAgentURL: `/v1/chat/completions`, queryAgentURL: `http://30.183.154.8:5000/v1/chat/completions`,
queryBody: {} queryBody: {
conv_uid: props.params?.agentId,
chat_mode: 'chat_normal'
},
initHistory: historyList?.data
}); });
return ( return (
<div className='mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl'> <div className='mx-auto flex h-full max-w-3xl flex-col gap-6 px-5 pt-6 sm:gap-8 xl:max-w-4xl'>
<ChatBoxComp messages={historyList?.data || []} onSubmit={handleChatSubmit}/> <ChatBoxComp
initialMessage={historyList?.data ? (historyList?.data?.length <= 0 ? props.searchParams?.initMessage : undefined) : undefined}
clearIntialMessage={() => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('initMessage');
window.history.replaceState(null, null, `?${searchParams.toString()}`);
}}
messages={history || []}
onSubmit={handleChatSubmit}
/>
</div> </div>
) )
} }

View File

@ -10,6 +10,7 @@ import {
Box, Box,
Stack, Stack,
Input, Input,
Textarea,
Chip, Chip,
styled styled
} from '@/lib/mui' } from '@/lib/mui'
@ -61,6 +62,8 @@ const Documents = () => {
const [documents, setDocuments] = useState<any>([]) const [documents, setDocuments] = useState<any>([])
const [webPageUrl, setWebPageUrl] = useState<string>('') const [webPageUrl, setWebPageUrl] = useState<string>('')
const [documentName, setDocumentName] = useState<any>('') const [documentName, setDocumentName] = useState<any>('')
const [textSource, setTextSource] = useState<string>('');
const [text, setText] = useState<string>('');
const [originFileObj, setOriginFileObj] = useState<any>(null) const [originFileObj, setOriginFileObj] = useState<any>(null)
const props: UploadProps = { const props: UploadProps = {
name: 'file', name: 'file',
@ -264,7 +267,6 @@ const Documents = () => {
onChange={(e: any) => setDocumentName(e.target.value)} onChange={(e: any) => setDocumentName(e.target.value)}
sx={{ marginBottom: '20px' }} sx={{ marginBottom: '20px' }}
/> />
Web Page URL:
{documentType === 'webPage' ? ( {documentType === 'webPage' ? (
<> <>
Web Page URL: Web Page URL:
@ -293,7 +295,20 @@ const Documents = () => {
</Dragger> </Dragger>
</> </>
) : ( ) : (
<></> <>
Text Source(Optional):
<Input
placeholder="Please input the text source"
onChange={(e: any) => setTextSource(e.target.value)}
sx={{ marginBottom: '20px' }}
/>
Text:
<Textarea
onChange={(e: any) => setText(e.target.value)}
minRows={4}
sx={{ marginBottom: '20px' }}
/>
</>
)} )}
</Box> </Box>
<Button <Button
@ -379,6 +394,47 @@ const Documents = () => {
} else { } else {
message.error(data.err_msg || 'failed') message.error(data.err_msg || 'failed')
} }
} else {
if (text === '') {
message.error('Please input the text')
return
}
const res = await fetch(
`http://localhost:8000/knowledge/${spaceName}/document/add`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
doc_name: documentName,
source: textSource,
content: text,
doc_type: 'TEXT'
})
}
)
const data = await res.json()
if (data.success) {
message.success('success')
setIsAddDocumentModalShow(false)
const res = await fetch(
`http://localhost:8000/knowledge/${spaceName}/document/list`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
}
)
const data = await res.json()
if (data.success) {
setDocuments(data.data)
}
} else {
message.error(data.err_msg || 'failed')
}
} }
}} }}
> >

View File

@ -13,6 +13,7 @@ import {
Stack, Stack,
Box, Box,
Input, Input,
Textarea,
styled styled
} from '@/lib/mui' } from '@/lib/mui'
@ -62,6 +63,8 @@ const Index = () => {
const [knowledgeSpaceName, setKnowledgeSpaceName] = useState<string>('') const [knowledgeSpaceName, setKnowledgeSpaceName] = useState<string>('')
const [webPageUrl, setWebPageUrl] = useState<string>('') const [webPageUrl, setWebPageUrl] = useState<string>('')
const [documentName, setDocumentName] = useState<any>('') const [documentName, setDocumentName] = useState<any>('')
const [textSource, setTextSource] = useState<string>('');
const [text, setText] = useState<string>('');
const [originFileObj, setOriginFileObj] = useState<any>(null) const [originFileObj, setOriginFileObj] = useState<any>(null)
const props: UploadProps = { const props: UploadProps = {
name: 'file', name: 'file',
@ -304,7 +307,20 @@ const Index = () => {
</Dragger> </Dragger>
</> </>
) : ( ) : (
<></> <>
Text Source(Optional):
<Input
placeholder="Please input the text source"
onChange={(e: any) => setTextSource(e.target.value)}
sx={{ marginBottom: '20px' }}
/>
Text:
<Textarea
onChange={(e: any) => setText(e.target.value)}
minRows={4}
sx={{ marginBottom: '20px' }}
/>
</>
)} )}
</Box> </Box>
<Button <Button
@ -362,6 +378,33 @@ const Index = () => {
} else { } else {
message.error(data.err_msg || 'failed') message.error(data.err_msg || 'failed')
} }
} else {
if (text === '') {
message.error('Please input the text')
return
}
const res = await fetch(
`http://localhost:8000/knowledge/${knowledgeSpaceName}/document/add`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
doc_name: documentName,
source: textSource,
content: text,
doc_type: 'TEXT'
})
}
)
const data = await res.json()
if (data.success) {
message.success('success')
setIsAddKnowledgeSpaceModalShow(false)
} else {
message.error(data.err_msg || 'failed')
}
} }
}} }}
> >

View File

@ -5,12 +5,12 @@ import LeftSider from '@/components/leftSider';
import { CssVarsProvider, ThemeProvider } from '@mui/joy/styles'; import { CssVarsProvider, ThemeProvider } from '@mui/joy/styles';
import { joyTheme } from './defaultTheme'; import { joyTheme } from './defaultTheme';
import TopProgressBar from '@/components/topProgressBar'; import TopProgressBar from '@/components/topProgressBar';
function RootLayout({
export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<html lang="en" className="h-full font-sans"> <html lang="en" className="h-full font-sans">
<body className={`h-full font-sans`}> <body className={`h-full font-sans`}>
@ -31,3 +31,5 @@ export default function RootLayout({
</html> </html>
) )
} }
export default RootLayout;

View File

@ -28,8 +28,7 @@ export default function Home() {
chat_mode: 'chat_normal' chat_mode: 'chat_normal'
}); });
if (res?.success && res?.data?.conv_uid) { if (res?.success && res?.data?.conv_uid) {
// router.push(`/agents/${res?.data?.conv_uid}?newMessage=${query}`); router.push(`/agents/${res?.data?.conv_uid}?initMessage=${query}`);
// await refreshDialogList();
} }
} catch (err) { } catch (err) {
} finally { } finally {

View File

@ -17,8 +17,9 @@ type Props = {
messages: Message[]; messages: Message[];
onSubmit: (message: string) => Promise<any>; onSubmit: (message: string) => Promise<any>;
messageTemplates?: string[]; messageTemplates?: string[];
initialMessage?: Message; initialMessage?: string;
readOnly?: boolean; readOnly?: boolean;
clearIntialMessage?: () => void;
}; };
const Schema = z.object({ query: z.string().min(1) }); const Schema = z.object({ query: z.string().min(1) });
@ -29,6 +30,7 @@ const ChatBoxComp = ({
messageTemplates, messageTemplates,
initialMessage, initialMessage,
readOnly, readOnly,
clearIntialMessage
}: Props) => { }: Props) => {
const scrollableRef = React.useRef<HTMLDivElement>(null); const scrollableRef = React.useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -52,6 +54,11 @@ const ChatBoxComp = ({
} }
}; };
const handleInitMessage = async () => {
await submit({ query: (initialMessage as string) });
clearIntialMessage?.();
}
React.useEffect(() => { React.useEffect(() => {
if (!scrollableRef.current) { if (!scrollableRef.current) {
return; return;
@ -61,11 +68,9 @@ const ChatBoxComp = ({
}, [messages?.length]); }, [messages?.length]);
React.useEffect(() => { React.useEffect(() => {
setTimeout(() => { if (initialMessage && messages.length <= 0) {
setFirstMsg( handleInitMessage();
initialMessage ? initialMessage : undefined }
);
}, 0);
}, [initialMessage]); }, [initialMessage]);
return ( return (
@ -116,7 +121,7 @@ const ChatBoxComp = ({
</Card> </Card>
)} )}
{messages.map((each, index) => ( {messages.filter(item => ['ai', 'human'].includes(item.role)).map((each, index) => (
<Stack <Stack
key={index} key={index}
sx={{ sx={{

View File

@ -1,14 +1,13 @@
"use client"; "use client";
import React, { useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent, Typography, Button, useColorScheme, Alert } from '@/lib/mui'; import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent, Typography, Button, useColorScheme } from '@/lib/mui';
import SmartToyRoundedIcon from '@mui/icons-material/SmartToyRounded'; // Icons import import SmartToyRoundedIcon from '@mui/icons-material/SmartToyRounded'; // Icons import
import StorageRoundedIcon from '@mui/icons-material/StorageRounded'; import StorageRoundedIcon from '@mui/icons-material/StorageRounded';
import DarkModeIcon from '@mui/icons-material/DarkMode'; import DarkModeIcon from '@mui/icons-material/DarkMode';
import WbSunnyIcon from '@mui/icons-material/WbSunny'; import WbSunnyIcon from '@mui/icons-material/WbSunny';
import HomeIcon from '@mui/icons-material/Home';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import { useQueryDialog } from '@/hooks/useQueryDialogue'; import { useQueryDialog } from '@/hooks/useQueryDialogue';
@ -16,16 +15,10 @@ import { useQueryDialog } from '@/hooks/useQueryDialogue';
const LeftSider = () => { const LeftSider = () => {
const pathname = usePathname(); const pathname = usePathname();
const { mode, setMode } = useColorScheme(); const { mode, setMode } = useColorScheme();
const [chatSelect, setChatSelect] = useState();
const { dialogueList } = useQueryDialog(); const { dialogueList } = useQueryDialog();
const menus = useMemo(() => { const menus = useMemo(() => {
return [{ return [{
label: 'Home',
icon: <HomeIcon fontSize="small" />,
route: '/',
active: pathname === '/',
}, {
label: 'Agents', label: 'Agents',
icon: <SmartToyRoundedIcon fontSize="small" />, icon: <SmartToyRoundedIcon fontSize="small" />,
route: '/agents', route: '/agents',
@ -53,7 +46,7 @@ const LeftSider = () => {
<MenuIcon /> <MenuIcon />
</div> </div>
<span className='truncate px-4'>New Chat</span> <span className='truncate px-4'>New Chat</span>
<a href='javascript: void(0)' className='-mr-3 flex h-9 w-9 shrink-0 items-center justify-center'> <a href='' className='-mr-3 flex h-9 w-9 shrink-0 items-center justify-center'>
<AddIcon /> <AddIcon />
</a> </a>
</nav> </nav>
@ -99,7 +92,9 @@ const LeftSider = () => {
px: 2 px: 2
}} }}
> >
<Button variant="outlined" color="primary" className='w-full'>+ </Button> <Link href={`/`}>
<Button variant="outlined" color="primary" className='w-full'>+ </Button>
</Link>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -121,26 +116,25 @@ const LeftSider = () => {
'& .JoyListItemButton-root': { p: '8px' }, '& .JoyListItemButton-root': { p: '8px' },
}} }}
> >
{dialogueList?.data?.map((each) => ( {dialogueList?.data?.map((each) => {
<ListItem key={each.conv_uid}> const isSelect = pathname === `/agents/${each.conv_uid}`;
<ListItemButton return (
selected={chatSelect === each.conv_uid} <ListItem key={each.conv_uid}>
variant={chatSelect === each.conv_uid ? 'soft' : 'plain'} <ListItemButton
onClick={() => { selected={isSelect}
setChatSelect(each.conv_uid); variant={isSelect ? 'soft' : 'plain'}
}} >
> <ListItemContent>
<ListItemContent> <Link href={`/agents/${each.conv_uid}`}>
<Link href={`/agents/${each.conv_uid}`}> <Typography fontSize={14} noWrap={true}>
<Typography fontSize={14} noWrap={true}> {each?.user_name || each?.user_input || 'undefined'}
{each?.user_name || each?.user_input || 'undefined'} </Typography>
</Typography> </Link>
</Link> </ListItemContent>
</ListItemButton>
</ListItemContent> </ListItem>
</ListItemButton> )
</ListItem> })}
))}
</List> </List>
</ListItem> </ListItem>
</List> </List>
@ -178,7 +172,6 @@ const LeftSider = () => {
color="neutral" color="neutral"
selected={each.active} selected={each.active}
variant={each.active ? 'soft' : 'plain'} variant={each.active ? 'soft' : 'plain'}
onClick={() => { setChatSelect(undefined); }}
> >
<ListItemDecorator <ListItemDecorator
sx={{ sx={{

View File

@ -2,35 +2,31 @@ import {
EventStreamContentType, EventStreamContentType,
fetchEventSource, fetchEventSource,
} from '@microsoft/fetch-event-source'; } from '@microsoft/fetch-event-source';
import { ApiError, ApiErrorType } from '@/utils/api-error';
import useStateReducer from './useStateReducer'; import useStateReducer from './useStateReducer';
import useVisitorId from './useVisitorId'; import { Message } from '@/types';
import { useEffect } from 'react';
type Props = { type Props = {
queryAgentURL: string; queryAgentURL: string;
queryHistoryURL?: string;
channel?: "dashboard" | "website" | "slack" | "crisp"; channel?: "dashboard" | "website" | "slack" | "crisp";
queryBody?: any; queryBody?: any;
initHistory: Message[];
}; };
const useAgentChat = ({ const useAgentChat = ({
queryAgentURL, queryAgentURL,
queryHistoryURL,
channel, channel,
queryBody, queryBody,
initHistory
}: Props) => { }: Props) => {
const [state, setState] = useStateReducer({ const [state, setState] = useStateReducer({
history: [{ history: (initHistory || []) as { role: 'human' | 'ai'; context: string; id?: string }[],
role: 'human',
context: 'hello',
}, {
role: 'agent',
context: 'Hello! How can I assist you today?',
}] as { role: 'human' | 'agent'; context: string; id?: string }[],
}); });
const { visitorId } = useVisitorId(); useEffect(() => {
if (initHistory) setState({ history: initHistory });
}, [initHistory]);
const handleChatSubmit = async (context: string) => { const handleChatSubmit = async (context: string) => {
if (!context) { if (!context) {
return; return;
@ -50,9 +46,6 @@ import {
const ctrl = new AbortController(); const ctrl = new AbortController();
let buffer = ''; let buffer = '';
class RetriableError extends Error {}
class FatalError extends Error {}
await fetchEventSource(queryAgentURL, { await fetchEventSource(queryAgentURL, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -60,9 +53,7 @@ import {
}, },
body: JSON.stringify({ body: JSON.stringify({
...queryBody, ...queryBody,
streaming: true, user_input: context,
query: context,
visitorId: visitorId,
channel, channel,
}), }),
signal: ctrl.signal, signal: ctrl.signal,
@ -79,64 +70,59 @@ import {
response.status !== 429 response.status !== 429
) { ) {
if (response.status === 402) { if (response.status === 402) {
throw new ApiError(ApiErrorType.USAGE_LIMIT); //throw new ApiError(ApiErrorType.USAGE_LIMIT);
} }
throw new FatalError(); // client-side errors are usually non-retriable:
//throw new FatalError();
} else { } else {
throw new RetriableError(); //throw new RetriableError();
} }
}, },
onclose() { onclose() {
throw new RetriableError(); // if the server closes the connection unexpectedly, retry:
console.log('onclose');
//throw new RetriableError();
}, },
onerror(err) { onerror(err) {
throw new Error(err); throw new Error(err);
// if (err instanceof FatalError) {
// ctrl.abort();
// throw new Error(); // rethrow to stop the operation
// } else if (err instanceof ApiError) {
// console.log('ApiError', ApiError);
// throw new Error();
// } else {
// throw new Error(err);
// }
}, },
onmessage: (event) => { onmessage: (event) => {
event.data = event.data.replaceAll('\\n', '\n');
console.log(event, 'event'); console.log(event, 'event');
if (event.data === '[DONE]') { if (event.data === '[DONE]') {
ctrl.abort(); ctrl.abort();
} else if (event.data?.startsWith('[ERROR]')) { } else if (event.data?.startsWith('[ERROR]')) {
ctrl.abort(); ctrl.abort();
setState({ setState({
history: [ history: [
...history, ...history,
{ {
role: 'agent', role: 'ai',
context: event.data.replace('[ERROR]', ''), context: event.data.replace('[ERROR]', ''),
} as any, } as any,
], ],
}); });
} else { } else {
buffer += decodeURIComponent(event.data) as string;
const h = [...history]; const h = [...history];
if (h?.[nextIndex]) { if (event.data) {
h[nextIndex].context = `${buffer}`; if (h?.[nextIndex]) {
} else { h[nextIndex].context = `${event.data}`;
h.push({ role: 'agent', context: buffer }); } else {
h.push({ role: 'ai', context: event.data });
}
setState({
history: h as any,
});
} }
setState({
history: h as any,
});
} }
}, },
}); });
} catch (err) { } catch (err) {
console.log('err', err);
setState({ setState({
history: [ history: [
...history, ...history,
{ role: 'agent', context: answer || '请求出错' as string }, { role: 'ai', context: answer || '请求出错' as string },
] as any, ] as any,
}); });
// if (err instanceof ApiError) { // if (err instanceof ApiError) {
@ -156,13 +142,12 @@ import {
// setState({ // setState({
// history: [ // history: [
// ...history, // ...history,
// { from: 'agent', message: answer as string }, // { from: 'ai', message: answer as string },
// ] as any, // ] as any,
// }); // });
// } // }
} }
}; };
return { return {
handleChatSubmit, handleChatSubmit,
history: state.history, history: state.history,

View File

@ -0,0 +1,10 @@
import { useState } from 'react';
export const useNewChat = () => {
const [message, setMessage] = useState<string | undefined>("hello");
return {
message,
setMessage
};
}

View File

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { isPlainObject } from 'lodash'; import { isPlainObject } from 'lodash';
axios.defaults.baseURL = 'http://30.183.153.244:5000'; axios.defaults.baseURL = 'http://30.183.154.8:5000';
axios.defaults.timeout = 10000; axios.defaults.timeout = 10000;