Merge branch 'new-page-framework' into dev_ty_06_end

This commit is contained in:
tuyang.yhj 2023-06-29 09:59:18 +08:00
commit 1acac4045f
21 changed files with 756 additions and 559 deletions

View File

@ -1,3 +1,8 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"indent": ["warning", 2, {
"SwitchCase": 1
}]
}
}

View File

@ -0,0 +1,25 @@
"use client"
import { useRequest } from 'ahooks';
import { sendGetRequest } from '@/utils/request';
import useAgentChat from '@/hooks/useAgentChat';
import ChatBoxComp from '@/components/chatBox';
const AgentPage = (props) => {
const { data: historyList } = useRequest(async () => await sendGetRequest('/v1/chat/dialogue/messages/history', {
con_uid: props.params?.agentId
}), {
ready: !!props.params?.agentId
});
const { handleChatSubmit, history } = useAgentChat({
queryAgentURL: `/v1/chat/completions`,
queryBody: {}
});
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'>
<ChatBoxComp messages={historyList?.data || []} onSubmit={handleChatSubmit}/>
</div>
)
}
export default AgentPage;

View File

@ -203,7 +203,7 @@ const Agents = () => {
</Stack>
</Grid>
<Grid xs={4}>
<ChatBoxComp messages={history} onSubmit={handleChatSubmit}/>
<ChatBoxComp messages={[]} onSubmit={handleChatSubmit}/>
</Grid>
</Grid>
</div>

View File

@ -32,7 +32,7 @@ const ChunkList = () => {
}, [])
return (
<div className="p-4">
<Table sx={{ '& thead th:nth-child(1)': { width: '40%' } }}>
<Table color="neutral" stripe="odd" variant="outlined">
<thead>
<tr>
<th>Name</th>

View File

@ -10,11 +10,25 @@ import {
Box,
Stack,
Input,
Chip,
styled
} from '@/lib/mui'
import moment from 'moment'
import { message } from 'antd'
import { InboxOutlined } from '@ant-design/icons'
import type { UploadProps } from 'antd'
import { Upload, message } from 'antd'
const { Dragger } = Upload
const Item = styled(Sheet)(({ theme }) => ({
width: '50%',
backgroundColor:
theme.palette.mode === 'dark' ? theme.palette.background.level1 : '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: 'center',
borderRadius: 4,
color: theme.vars.palette.text.secondary
}))
const stepsOfAddingDocument = [
'Choose a Datasource type',
'Setup the Datasource'
@ -36,16 +50,6 @@ const documentTypeList = [
subTitle: 'It can be: PDF, CSV, JSON, Text, PowerPoint, Word, Excel'
}
]
const Item = styled(Sheet)(({ theme }) => ({
width: '50%',
backgroundColor:
theme.palette.mode === 'dark' ? theme.palette.background.level1 : '#fff',
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: 'center',
borderRadius: 4,
color: theme.vars.palette.text.secondary
}))
const Documents = () => {
const router = useRouter()
@ -53,9 +57,25 @@ const Documents = () => {
const [isAddDocumentModalShow, setIsAddDocumentModalShow] =
useState<boolean>(false)
const [activeStep, setActiveStep] = useState<number>(0)
const [documentType, setDocumentType] = useState<string>('')
const [documents, setDocuments] = useState<any>([])
const [webPageUrl, setWebPageUrl] = useState<string>('')
const [documentName, setDocumentName] = useState<string>('')
const [documentName, setDocumentName] = useState<any>('')
const [originFileObj, setOriginFileObj] = useState<any>(null)
const props: UploadProps = {
name: 'file',
multiple: false,
onChange(info) {
console.log(info)
if (info.fileList.length === 0) {
setOriginFileObj(null)
setDocumentName('')
return
}
setOriginFileObj(info.file.originFileObj)
setDocumentName(info.file.originFileObj?.name)
}
}
useEffect(() => {
async function fetchDocuments() {
const res = await fetch(
@ -90,7 +110,7 @@ const Documents = () => {
+ Add Datasource
</Button>
</Sheet>
<Table sx={{ '& thead th:nth-child(1)': { width: '40%' } }}>
<Table color="neutral" stripe="odd" variant="outlined">
<thead>
<tr>
<th>Name</th>
@ -108,12 +128,30 @@ const Documents = () => {
<td>{row.doc_type}</td>
<td>{row.chunk_size}</td>
<td>{moment(row.last_sync).format('YYYY-MM-DD HH:MM:SS')}</td>
<td>{row.status}</td>
<td>
<Chip
color={(function () {
switch (row.status) {
case 'TODO':
return 'neutral'
case 'RUNNING':
return 'primary'
case 'FINISHED':
return 'success'
case 'FAILED':
return 'danger'
}
})()}
>
{row.status}
</Chip>
</td>
<td>
{
<>
<Button
variant="outlined"
size="sm"
onClick={async () => {
const res = await fetch(
`http://localhost:8000/knowledge/${spaceName}/document/sync`,
@ -139,6 +177,7 @@ const Documents = () => {
</Button>
<Button
variant="outlined"
size="sm"
onClick={() => {
router.push(
`/datastores/documents/chunklist?spacename=${spaceName}&documentid=${row.id}`
@ -204,9 +243,8 @@ const Documents = () => {
cursor: 'pointer'
}}
onClick={() => {
if (item.type === 'webPage') {
setDocumentType(item.type)
setActiveStep(1)
}
}}
>
<Sheet sx={{ fontSize: '20px', fontWeight: 'bold' }}>
@ -226,11 +264,37 @@ const Documents = () => {
onChange={(e: any) => setDocumentName(e.target.value)}
sx={{ marginBottom: '20px' }}
/>
Web Page URL:
{documentType === 'webPage' ? (
<>
Web Page URL:
<Input
placeholder="Please input the Web Page URL"
onChange={(e: any) => setWebPageUrl(e.target.value)}
/>
</>
) : documentType === 'file' ? (
<>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p
style={{ color: 'rgb(22, 108, 255)', fontSize: '20px' }}
>
Select or Drop file
</p>
<p
className="ant-upload-hint"
style={{ color: 'rgb(22, 108, 255)' }}
>
PDF, PowerPoint, Excel, Word, Text, Markdown,
</p>
</Dragger>
</>
) : (
<></>
)}
</Box>
<Button
onClick={async () => {
@ -238,6 +302,7 @@ const Documents = () => {
message.error('Please input the name')
return
}
if (documentType === 'webPage') {
if (webPageUrl === '') {
message.error('Please input the Web Page URL')
return
@ -277,6 +342,44 @@ const Documents = () => {
} else {
message.error(data.err_msg || 'failed')
}
} else if (documentType === 'file') {
if (!originFileObj) {
message.error('Please select a file')
return
}
const formData = new FormData();
formData.append('doc_name', documentName);
formData.append('doc_file', originFileObj);
formData.append('doc_type', 'DOCUMENT');
const res = await fetch(
`http://localhost:8000/knowledge/${spaceName}/document/upload`,
{
method: 'POST',
body: formData
}
)
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')
}
}
}}
>
Finish

View File

@ -2,7 +2,9 @@
import { useRouter } from 'next/navigation'
import React, { useState, useEffect } from 'react'
import { message } from 'antd'
import { InboxOutlined } from '@ant-design/icons'
import type { UploadProps } from 'antd'
import { message, Upload } from 'antd'
import {
Modal,
Button,
@ -14,6 +16,8 @@ import {
styled
} from '@/lib/mui'
const { Dragger } = Upload
const Item = styled(Sheet)(({ theme }) => ({
width: '33%',
backgroundColor:
@ -51,12 +55,28 @@ const documentTypeList = [
const Index = () => {
const router = useRouter()
const [activeStep, setActiveStep] = useState<number>(0)
const [documentType, setDocumentType] = useState<string>('')
const [knowledgeSpaceList, setKnowledgeSpaceList] = useState<any>([])
const [isAddKnowledgeSpaceModalShow, setIsAddKnowledgeSpaceModalShow] =
useState<boolean>(false)
const [knowledgeSpaceName, setKnowledgeSpaceName] = useState<string>('')
const [webPageUrl, setWebPageUrl] = useState<string>('')
const [documentName, setDocumentName] = useState<string>('')
const [documentName, setDocumentName] = useState<any>('')
const [originFileObj, setOriginFileObj] = useState<any>(null)
const props: UploadProps = {
name: 'file',
multiple: false,
onChange(info) {
console.log(info)
if (info.fileList.length === 0) {
setOriginFileObj(null)
setDocumentName('')
return
}
setOriginFileObj(info.file.originFileObj)
setDocumentName(info.file.originFileObj?.name)
}
}
useEffect(() => {
async function fetchData() {
const res = await fetch('http://localhost:8000/knowledge/space/list', {
@ -75,14 +95,21 @@ const Index = () => {
}, [])
return (
<>
<Sheet sx={{
display: "flex",
justifyContent: "space-between"
}} className="p-4">
<Sheet sx={{
<Sheet
sx={{
display: 'flex',
justifyContent: 'space-between'
}}
className="p-4"
>
<Sheet
sx={{
fontSize: '30px',
fontWeight: 'bold'
}}>Knowledge Spaces</Sheet>
}}
>
Knowledge Spaces
</Sheet>
<Button
onClick={() => setIsAddKnowledgeSpaceModalShow(true)}
variant="outlined"
@ -91,7 +118,7 @@ const Index = () => {
</Button>
</Sheet>
<div className="page-body p-4">
<Table sx={{ '& thead th:nth-child(1)': { width: '40%' } }}>
<Table color="neutral" stripe="odd" variant="outlined">
<thead>
<tr>
<th>Name</th>
@ -228,12 +255,13 @@ const Index = () => {
cursor: 'pointer'
}}
onClick={() => {
if (item.type === 'webPage') {
setActiveStep(2);
}
setDocumentType(item.type)
setActiveStep(2)
}}
>
<Sheet sx={{ fontSize: '20px', fontWeight: 'bold' }}>{item.title}</Sheet>
<Sheet sx={{ fontSize: '20px', fontWeight: 'bold' }}>
{item.title}
</Sheet>
<Sheet>{item.subTitle}</Sheet>
</Sheet>
))}
@ -248,21 +276,47 @@ const Index = () => {
onChange={(e: any) => setDocumentName(e.target.value)}
sx={{ marginBottom: '20px' }}
/>
{documentType === 'webPage' ? (
<>
Web Page URL:
<Input
placeholder="Please input the Web Page URL"
onChange={(e: any) => setWebPageUrl(e.target.value)}
/>
</>
) : documentType === 'file' ? (
<>
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p
style={{ color: 'rgb(22, 108, 255)', fontSize: '20px' }}
>
Select or Drop file
</p>
<p
className="ant-upload-hint"
style={{ color: 'rgb(22, 108, 255)' }}
>
PDF, PowerPoint, Excel, Word, Text, Markdown,
</p>
</Dragger>
</>
) : (
<></>
)}
</Box>
<Button
onClick={async () => {
if (documentName === '') {
message.error('Please input the name');
return;
message.error('Please input the name')
return
}
if (documentType === 'webPage') {
if (webPageUrl === '') {
message.error('Please input the Web Page URL');
return;
message.error('Please input the Web Page URL')
return
}
const res = await fetch(
`http://localhost:8000/knowledge/${knowledgeSpaceName}/document/add`,
@ -285,6 +339,30 @@ const Index = () => {
} else {
message.error(data.err_msg || 'failed')
}
} else if (documentType === 'file') {
if (!originFileObj) {
message.error('Please select a file')
return
}
const formData = new FormData();
formData.append('doc_name', documentName);
formData.append('doc_file', originFileObj);
formData.append('doc_type', 'DOCUMENT');
const res = await fetch(
`http://localhost:8000/knowledge/${knowledgeSpaceName}/document/upload`,
{
method: 'POST',
body: formData
}
)
const data = await res.json()
if (data.success) {
message.success('success')
setIsAddKnowledgeSpaceModalShow(false)
} else {
message.error(data.err_msg || 'failed')
}
}
}}
>
Finish

View File

@ -10,7 +10,7 @@ export const joyTheme = extendTheme({
...colors.purple,
},
neutral: {
plainColor: '#25252D',
plainColor: '#4d4d4d',
plainHoverColor: '#131318',
plainHoverBg: '#EBEBEF',
plainActiveBg: '#D8D8DF',
@ -42,7 +42,7 @@ export const joyTheme = extendTheme({
primary: '#EBEBEF'
},
background: {
body: '#09090D',
body: '#0f172a',
surface: '#1e293b40'
}
},

View File

@ -2,9 +2,6 @@
@tailwind components;
@tailwind utilities;
a {
color: #1677ff;
}
body {
margin: 0;

View File

@ -11,17 +11,18 @@ export default function RootLayout({
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="min-h-full font-sans">
<body className={`min-h-screen font-sans`}>
<html lang="en" className="h-full font-sans">
<body className={`h-full font-sans`}>
<ThemeProvider theme={joyTheme}>
<CssVarsProvider theme={joyTheme} defaultMode="light">
<TopProgressBar />
<div className='min-h-screen flex flex-col'>
<div className="flex flex-1 flex-row">
<div className={`contents h-full`}>
<div className="grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd dark:text-gray-300 md:grid-cols-[280px,1fr] md:grid-rows-[1fr]">
<LeftSider />
<div className='flex-1 overflow-auto'>{children}</div>
<div className='relative min-h-0 min-w-0'>
{children}
</div>
</div>
</div>
</CssVarsProvider>

View File

@ -1,267 +1,98 @@
"use client";
import ReactMarkdown from 'react-markdown';
import { Collapse } from 'antd';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import { Box, Slider, Input, Tabs, TabList, Typography, TabPanel, Tab, RadioGroup, Radio, tabClasses, useColorScheme, radioClasses } from '@/lib/mui';
import { sendGetRequest } from '@/utils/request';
import { useEffect, useState } from 'react';
import ChatBoxComp from '@/components/chatBox';
import useAgentChat from '@/hooks/useAgentChat';
import { useState } from 'react';
import { Button, Input, useColorScheme } from '@/lib/mui';
import IconButton from '@mui/joy/IconButton';
import SendRoundedIcon from '@mui/icons-material/SendRounded';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { sendPostRequest } from '@/utils/request';
import { useRouter } from 'next/navigation';
import { useQueryDialog } from '@/hooks/useQueryDialogue';
export default function Home() {
const [temperatureNum, setTemperatureNum] = useState(3);
const [tokenSize, setTokenSize] = useState(0);
const { mode, setMode } = useColorScheme();
const { handleChatSubmit, history } = useAgentChat({
queryAgentURL: `/api/agents/query`,
const Schema = z.object({ query: z.string().min(1) });
const router = useRouter();
const { mode } = useColorScheme();
const [isLoading, setIsLoading] = useState(false);
const methods = useForm<z.infer<typeof Schema>>({
resolver: zodResolver(Schema),
defaultValues: {},
});
const handleGetD = () => {
sendGetRequest('/v1/chat/dialogue/list', {
})
const { refreshDialogList } = useQueryDialog();
const submit = async ({ query }: z.infer<typeof Schema>) => {
try {
setIsLoading(true);
methods.reset();
const res = await sendPostRequest('/v1/chat/dialogue/new', {
chat_mode: 'chat_normal'
});
if (res?.success && res?.data?.conv_uid) {
// router.push(`/agents/${res?.data?.conv_uid}?newMessage=${query}`);
// await refreshDialogList();
}
} catch (err) {
} finally {
setIsLoading(false);
}
};
useEffect(() => {
handleGetD();
}, []);
return (
<div className='p-6 w-full h-full text-sm flex flex-col gap-4'>
<ReactMarkdown linkTarget={'_blank'}>
[DB-GPT](https://github.com/csunny/DB-GPT) 是一个开源的以数据库为基础的GPT实验项目使用本地化的GPT大模型与您的数据和环境进行交互无数据泄露风险100% 私密100% 安全。
</ReactMarkdown>
<Box
sx={{
'& .ant-collapse': {
backgroundColor: 'var(--joy-palette-background-level1)',
color: 'var(--joy-palette-neutral-plainColor)'
},
'& .ant-collapse>.ant-collapse-item >.ant-collapse-header, & .ant-collapse .ant-collapse-content': {
color: 'var(--joy-palette-neutral-plainColor)'
},
'& .ant-collapse .ant-collapse-content>.ant-collapse-content-box': {
paddingTop: 0
}
}}
>
<Collapse
collapsible="header"
defaultActiveKey={['1']}
ghost
bordered
expandIcon={({ isActive }) => (
<div className={`text-2xl cursor-pointer ${isActive ? 'rotate-0' : 'rotate-90'}`}>
<ArrowDropDownIcon />
</div>
)}
expandIconPosition="end"
items={[
{
key: '1',
label: 'This panel can only be collapsed by clicking text',
children: (
<>
<Box
className="p-4 border"
sx={{
borderColor: 'var(--joy-palette-background-level2)',
}}
>
<div className='flex flex-row justify-between items-center'>
<span>Temperature</span>
<Input
size='sm'
type="number"
value={temperatureNum / 10}
onChange={(e) => {
setTemperatureNum(Number(e.target.value) * 10);
}}
slotProps={{
input: {
min: 0,
max: 1,
step: 0.1,
},
}}
/>
<div className={`${mode} absolute z-20 top-0 inset-x-0 flex justify-center overflow-hidden pointer-events-none`}>
<div className='w-[108rem] flex-none flex justify-end'>
<picture>
<source srcSet='/bg1.avif' type='image/avif'></source>
<img srcSet='/bg2.png' alt="" className='w-[71.75rem] flex-none max-w-none '/>
</picture>
</div>
<Slider
color="info"
value={temperatureNum}
max={10}
onChange={(e, value) => {
setTemperatureNum(value);
}}
/>
</Box>
<Box
className="p-4 border border-t-0"
sx={{
borderColor: 'var(--joy-palette-background-level2)',
}}
>
<div className='flex flex-row justify-between items-center'>
<span>Maximum output token size</span>
<Input
size="sm"
type="number"
value={tokenSize}
onChange={(e) => {
setTokenSize(Number(e.target.value));
}}
slotProps={{
input: {
min: 0,
max: 1024,
step: 64,
},
}}
/>
</div>
<Slider
color="info"
value={tokenSize}
max={1024}
step={64}
onChange={(e, value) => {
setTokenSize(value);
}}
/>
</Box>
</>
),
},
]}
/>
</Box>
<Box>
<Tabs
className='w-full'
aria-label="Pricing plan"
defaultValue={0}
sx={(theme) => ({
'--Tabs-gap': '0px',
borderRadius: 'unset',
boxShadow: 'sm',
overflow: 'auto',
WebkitBoxShadow: 'none'
})}
>
<TabList
sx={{
'--ListItem-radius': '0px',
borderRadius: 0,
bgcolor: 'background.body',
[`& .${tabClasses.root}`]: {
fontWeight: 'lg',
flex: 'unset',
position: 'relative',
[`&.${tabClasses.selected}`]: {
border: '1px solid var(--joy-palette-background-level2)',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
},
[`&.${tabClasses.selected}:before`]: {
content: '""',
display: 'block',
position: 'absolute',
bottom: -4,
<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='lg:my-auto grid gap-8 lg:grid-cols-3'>
<div className='lg:col-span-3 lg:mt-12'>
<p className='mb-3'>Scenes</p>
<div className='grid gap-2 lg:grid-cols-4 lg:gap-5'>
<Button size="md" variant="soft">LLM native dialogue</Button>
<Button size="md" variant="soft">Default documents</Button>
<Button size="md" variant="soft">New documents</Button>
<Button size="md" variant="soft">Chat with url</Button>
</div>
</div>
</div>
<div className='h-60 flex-none'></div>
</div>
<div className='pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center px-3.5 py-4 max-md:border-t sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto'>
<form
style={{
maxWidth: '100%',
width: '100%',
height: 6,
bgcolor: mode == 'dark' ? '#000' : 'var(--joy-palette-background-surface)',
},
[`&.${tabClasses.focusVisible}`]: {
outlineOffset: '-3px',
},
},
position: 'relative',
display: 'flex',
marginTop: 'auto',
overflow: 'visible',
background: 'none',
justifyContent: 'center',
marginLeft: 'auto',
marginRight: 'auto',
}}
onSubmit={(e) => {
methods.handleSubmit(submit)(e);
}}
>
<Tab sx={{ py: 1.5 }}>Documents Chat</Tab>
<Tab>SQL Generation & Diagnostics</Tab>
<Tab>Plugin Mode</Tab>
</TabList>
<TabPanel
value={0}
sx={{
p: 3,
border: '1px solid var(--joy-palette-background-level2)',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
}}
>
<RadioGroup
orientation="horizontal"
defaultValue="LLM native dialogue"
name="radio-buttons-group"
className='gap-3 p-3'
sx={{
backgroundColor: 'var(--joy-palette-background-level1)',
'& .MuiRadio-radio': {
borderColor: '#707070'
},
}}
>
<Box className="px-2 py-1 border rounded" sx={{ borderColor: 'var(--joy-palette-background-level2)' }}>
<Radio value="LLM native dialogue" label="LLM native dialogue" variant="outlined"
sx={{
borderColor: 'var(--joy-palette-neutral-outlinedHoverColor)',
}}
<Input
sx={{ width: '100%' }}
variant="outlined"
placeholder='Ask anything'
endDecorator={
<IconButton type="submit" disabled={isLoading}>
<SendRoundedIcon />
</IconButton>
}
{...methods.register('query')}
/>
</Box>
<Box className="px-2 py-1 border rounded" sx={{ borderColor: 'var(--joy-palette-background-level2)' }}>
<Radio value="Default documents" label="Default documents" variant="outlined" />
</Box>
<Box className="px-2 py-1 border rounded" sx={{ borderColor: 'var(--joy-palette-background-level2)' }}>
<Radio value="New documents" label="New documents" variant="outlined" />
</Box>
<Box className="px-2 py-1 border rounded" sx={{ borderColor: 'var(--joy-palette-background-level2)' }}>
<Radio value="Chat with url" label="Chat with url" variant="outlined" />
</Box>
</RadioGroup>
</TabPanel>
<TabPanel value={1} sx={{ p: 3 }}>
<Typography level="inherit">
Best for professional developers building enterprise or data-rich
applications.
</Typography>
<Typography textColor="primary.400" fontSize="xl3" fontWeight="xl" my={1}>
$15{' '}
<Typography fontSize="sm" textColor="text.secondary" fontWeight="md">
/ dev / month
</Typography>
</Typography>
</TabPanel>
<TabPanel value={2} sx={{ p: 3 }}>
<Typography level="inherit">
The most advanced features for data-rich applications, as well as the
highest priority for support.
</Typography>
<Typography textColor="primary.400" fontSize="xl3" fontWeight="xl" my={1}>
<Typography
fontSize="xl"
borderRadius="sm"
px={0.5}
mr={0.5}
sx={(theme) => ({
...theme.variants.soft.danger,
color: 'danger.400',
verticalAlign: 'text-top',
textDecoration: 'line-through',
})}
>
$49
</Typography>
$37*{' '}
<Typography fontSize="sm" textColor="text.secondary" fontWeight="md">
/ dev / month
</Typography>
</Typography>
</TabPanel>
</Tabs>
</Box>
<ChatBoxComp messages={history} onSubmit={handleChatSubmit}/>
</form>
</div>
</>
)
}

View File

@ -17,7 +17,7 @@ type Props = {
messages: Message[];
onSubmit: (message: string) => Promise<any>;
messageTemplates?: string[];
initialMessage?: string;
initialMessage?: Message;
readOnly?: boolean;
};
@ -63,7 +63,7 @@ const ChatBoxComp = ({
React.useEffect(() => {
setTimeout(() => {
setFirstMsg(
initialMessage ? { from: 'agent', message: initialMessage } : undefined
initialMessage ? initialMessage : undefined
);
}, 0);
}, [initialMessage]);
@ -112,7 +112,7 @@ const ChatBoxComp = ({
whiteSpace: 'pre-wrap',
}}
>
{firstMsg?.message}
{firstMsg?.context}
</Card>
)}
@ -120,17 +120,17 @@ const ChatBoxComp = ({
<Stack
key={index}
sx={{
mr: each.from === 'agent' ? 'auto' : 'none',
ml: each.from === 'human' ? 'auto' : 'none',
mr: each.role === 'ai' ? 'auto' : 'none',
ml: each.role === 'human' ? 'auto' : 'none',
}}
>
<Card
size="sm"
variant={'outlined'}
className={
each.from === 'agent' ? 'message-agent' : 'message-human'
each.role === 'ai' ? 'message-agent' : 'message-human'
}
color={each.from === 'agent' ? 'primary' : 'neutral'}
color={each.role === 'ai' ? 'primary' : 'neutral'}
sx={(theme) => ({
px: 2,
'ol, ul': {
@ -153,7 +153,7 @@ const ChatBoxComp = ({
})}
>
<ReactMarkdown remarkPlugins={[remarkGfm]} linkTarget={'_blank'}>
{each.message}
{each.context}
</ReactMarkdown>
</Card>
</Stack>

View File

@ -3,42 +3,26 @@ import React, { useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent, Typography, Button, useColorScheme } from '@/lib/mui';
import { Box, List, ListItem, ListItemButton, ListItemDecorator, ListItemContent, Typography, Button, useColorScheme, Alert } from '@/lib/mui';
import SmartToyRoundedIcon from '@mui/icons-material/SmartToyRounded'; // Icons import
import StorageRoundedIcon from '@mui/icons-material/StorageRounded';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import WbSunnyIcon from '@mui/icons-material/WbSunny';
const mockHistory = [{
id: 1,
summary: "chat1",
history: [{
from: 'human',
message: 'Hello'
}, {
from: 'agent',
message: 'Hello! How can I assist you today?'
}]
}, {
id: 2,
summary: "天气",
history: [{
from: 'human',
message: '讲个笑话'
}, {
from: 'agent',
message: '当然这是一个经典的笑话xxx'
}]
}];
import HomeIcon from '@mui/icons-material/Home';
import MenuIcon from '@mui/icons-material/Menu';
import AddIcon from '@mui/icons-material/Add';
import { useQueryDialog } from '@/hooks/useQueryDialogue';
const LeftSider = () => {
const pathname = usePathname();
const { mode, setMode } = useColorScheme();
const [chatSelect, setChatSelect] = useState();
const { dialogueList } = useQueryDialog();
const menus = useMemo(() => {
return [{
label: 'Home',
icon: <SmartToyRoundedIcon fontSize="small" />,
icon: <HomeIcon fontSize="small" />,
route: '/',
active: pathname === '/',
}, {
@ -63,6 +47,17 @@ const LeftSider = () => {
};
return (
<>
<nav className='flex h-12 items-center justify-between border-b bg-gray-50 px-4 dark:border-gray-800 dark:bg-gray-800/70 md:hidden'>
<div>
<MenuIcon />
</div>
<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'>
<AddIcon />
</a>
</nav>
<nav className="grid max-h-screen h-full max-md:hidden">
<Box
sx={{
display: 'flex',
@ -86,11 +81,11 @@ const LeftSider = () => {
alignItems: 'center',
}}
>
<div className='flex items-center justify-center gap-3'>
<div className='flex items-center gap-3'>
<Image
src="/databerry-logo-icon.png"
width="200"
height="200"
width="100"
height="100"
className='w-12'
alt="Databerry"
/>
@ -126,16 +121,23 @@ const LeftSider = () => {
'& .JoyListItemButton-root': { p: '8px' },
}}
>
{mockHistory.map((each, index) => (
<ListItem key={index}>
{dialogueList?.data?.map((each) => (
<ListItem key={each.conv_uid}>
<ListItemButton
selected={chatSelect === each.id}
variant={chatSelect === each.id ? 'soft' : 'plain'}
selected={chatSelect === each.conv_uid}
variant={chatSelect === each.conv_uid ? 'soft' : 'plain'}
onClick={() => {
setChatSelect(each.id);
setChatSelect(each.conv_uid);
}}
>
<ListItemContent>{each.summary}</ListItemContent>
<ListItemContent>
<Link href={`/agents/${each.conv_uid}`}>
<Typography fontSize={14} noWrap={true}>
{each?.user_name || each?.user_input || 'undefined'}
</Typography>
</Link>
</ListItemContent>
</ListItemButton>
</ListItem>
))}
@ -176,6 +178,7 @@ const LeftSider = () => {
color="neutral"
selected={each.active}
variant={each.active ? 'soft' : 'plain'}
onClick={() => { setChatSelect(undefined); }}
>
<ListItemDecorator
sx={{
@ -208,8 +211,9 @@ const LeftSider = () => {
</List>
</Box>
</div>
</Box>
</nav>
</>
)
};

View File

@ -21,22 +21,22 @@ import {
}: Props) => {
const [state, setState] = useStateReducer({
history: [{
from: 'human',
message: 'hello',
role: 'human',
context: 'hello',
}, {
from: 'agent',
message: 'Hello! How can I assist you today?',
}] as { from: 'human' | 'agent'; message: string; id?: string }[],
role: 'agent',
context: 'Hello! How can I assist you today?',
}] as { role: 'human' | 'agent'; context: string; id?: string }[],
});
const { visitorId } = useVisitorId();
const handleChatSubmit = async (message: string) => {
if (!message) {
const handleChatSubmit = async (context: string) => {
if (!context) {
return;
}
const history = [...state.history, { from: 'human', message }];
const history = [...state.history, { role: 'human', context }];
const nextIndex = history.length;
setState({
@ -61,7 +61,7 @@ import {
body: JSON.stringify({
...queryBody,
streaming: true,
query: message,
query: context,
visitorId: visitorId,
channel,
}),
@ -112,8 +112,8 @@ import {
history: [
...history,
{
from: 'agent',
message: event.data.replace('[ERROR]', ''),
role: 'agent',
context: event.data.replace('[ERROR]', ''),
} as any,
],
});
@ -121,9 +121,9 @@ import {
buffer += decodeURIComponent(event.data) as string;
const h = [...history];
if (h?.[nextIndex]) {
h[nextIndex].message = `${buffer}`;
h[nextIndex].context = `${buffer}`;
} else {
h.push({ from: 'agent', message: buffer });
h.push({ role: 'agent', context: buffer });
}
setState({
history: h as any,
@ -136,7 +136,7 @@ import {
setState({
history: [
...history,
{ from: 'agent', message: answer || '请求出错' as string },
{ role: 'agent', context: answer || '请求出错' as string },
] as any,
});
// if (err instanceof ApiError) {

View File

@ -0,0 +1,20 @@
import { useRequest } from 'ahooks';
import { sendGetRequest } from '@/utils/request';
export function useQueryDialog() {
const {
run: queryDialogueList,
data: dialogueList,
loading: loadingDialogList,
refresh: refreshDialogList,
} = useRequest(async () => await sendGetRequest('/v1/chat/dialogue/list'), {
manual: false,
});
return {
queryDialogueList,
dialogueList,
loadingDialogList,
refreshDialogList
};
}

View File

@ -24,6 +24,7 @@
"@types/node": "20.3.1",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
"ahooks": "^3.7.8",
"antd": "^5.6.2",
"autoprefixer": "10.4.14",
"axios": "^1.3.4",
@ -31,6 +32,7 @@
"cuid": "^3.0.0",
"eslint": "8.43.0",
"eslint-config-next": "13.4.7",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"next": "13.4.7",
"next-auth": "^4.20.1",
@ -48,6 +50,7 @@
"zod": "^3.19.1"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/nprogress": "^0.2.0"
}
},
@ -2244,11 +2247,22 @@
"@types/unist": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/lodash": {
"version": "4.14.195",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz",
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
"dev": true
},
"node_modules/@types/mdast": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.11.tgz",
@ -2443,6 +2457,34 @@
"object-assign": "4.x"
}
},
"node_modules/ahooks": {
"version": "3.7.8",
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.7.8.tgz",
"integrity": "sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^2.x.x",
"ahooks-v3-count": "^1.0.0",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^2.x.x",
"lodash": "^4.17.21",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/ahooks-v3-count": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz",
"integrity": "sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ=="
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@ -4566,6 +4608,11 @@
"node": ">= 0.4"
}
},
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
@ -4856,6 +4903,11 @@
"resolved": "https://registry.npmmirror.com/jose/-/jose-4.14.4.tgz",
"integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g=="
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -5018,7 +5070,7 @@
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.debounce": {
@ -7308,6 +7360,14 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/scroll-into-view-if-needed": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz",
@ -9888,11 +9948,22 @@
"@types/unist": "*"
}
},
"@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"@types/lodash": {
"version": "4.14.195",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz",
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
"dev": true
},
"@types/mdast": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.11.tgz",
@ -10049,6 +10120,28 @@
"object-assign": "4.x"
}
},
"ahooks": {
"version": "3.7.8",
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.7.8.tgz",
"integrity": "sha512-e/NMlQWoCjaUtncNFIZk3FG1ImSkV/JhScQSkTqnftakRwdfZWSw6zzoWSG9OMYqPNs2MguDYBUFFC6THelWXA==",
"requires": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^2.x.x",
"ahooks-v3-count": "^1.0.0",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^2.x.x",
"lodash": "^4.17.21",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
}
},
"ahooks-v3-count": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/ahooks-v3-count/-/ahooks-v3-count-1.0.0.tgz",
"integrity": "sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@ -11763,6 +11856,11 @@
"side-channel": "^1.0.4"
}
},
"intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="
},
"is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.1.1.tgz",
@ -11980,6 +12078,11 @@
"resolved": "https://registry.npmmirror.com/jose/-/jose-4.14.4.tgz",
"integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g=="
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -12106,7 +12209,7 @@
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.debounce": {
@ -13904,6 +14007,11 @@
"loose-envify": "^1.1.0"
}
},
"screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="
},
"scroll-into-view-if-needed": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz",

View File

@ -25,6 +25,7 @@
"@types/node": "20.3.1",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
"ahooks": "^3.7.8",
"antd": "^5.6.2",
"autoprefixer": "10.4.14",
"axios": "^1.3.4",
@ -32,6 +33,7 @@
"cuid": "^3.0.0",
"eslint": "8.43.0",
"eslint-config-next": "13.4.7",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"next": "13.4.7",
"next-auth": "^4.20.1",
@ -49,6 +51,7 @@
"zod": "^3.19.1"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/nprogress": "^0.2.0"
}
}

BIN
datacenter/public/bg1.avif Normal file

Binary file not shown.

BIN
datacenter/public/bg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@ -14,5 +14,6 @@ module.exports = {
}
},
},
darkMode: 'class',
plugins: [],
}

View File

@ -1,7 +1,7 @@
import { NextApiRequest, NextPage } from 'next/types';
import { Session } from 'next-auth';
export type Message = { from: 'human' | 'agent'; message: string; createdAt?: Date };
export type Message = { role: 'human' | 'ai'; context: string; createdAt?: Date };
export type AppNextApiRequest = NextApiRequest & {
session: Session;

View File

@ -1,4 +1,5 @@
import axios from 'axios';
import { isPlainObject } from 'lodash';
axios.defaults.baseURL = 'http://30.183.153.244:5000';
@ -9,6 +10,24 @@ axios.interceptors.response.use(
err => Promise.reject(err)
);
const DEFAULT_HEADERS = {
'content-type': 'application/json',
};
// body 字段 trim
const sanitizeBody = (obj: Record<string, any>): string => {
// simple shallow copy to avoid changing original obj
if (!isPlainObject(obj)) return JSON.stringify(obj);
const resObj = { ...obj };
for (const key in resObj) {
const val = resObj[key];
if (typeof val === 'string') {
resObj[key] = val.trim();
}
}
return JSON.stringify(resObj);
};
export const sendGetRequest = (url: string, qs?: { [key: string]: any }) => {
if (qs) {
const str = Object.keys(qs)
@ -19,13 +38,15 @@ export const sendGetRequest = (url: string, qs?: { [key: string]: any }) => {
url += `?${str}`;
}
}
axios.get(url, {
headers: {
"Content-Type": 'text/plain'
return axios.get(url, {
headers: DEFAULT_HEADERS
}).then(res => res).catch(err => Promise.reject(err));
}
}).then(res => {
console.log(res, 'res');
}).catch(err => {
console.log(err, 'err');
})
export const sendPostRequest = (url: string, body?: any) => {
const reqBody = sanitizeBody(body);
return axios.post(url, {
body: reqBody,
headers: DEFAULT_HEADERS
}).then(res => res).catch(err => Promise.reject(err));
}