Commit 2bfb77ea authored by Joel's avatar Joel

Merge branch 'feat/universal-chat-fe' into deploy/dev

parents 64f7e622 9be4b804
......@@ -9,8 +9,6 @@ import { DataSet as DataSetIcon, Loading as LodingIcon, Search, ThoughtList, Web
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import type { DataSet } from '@/models/datasets'
// https://www.freecodecamp.org/news/how-to-write-a-regular-expression-for-a-url/
const urlRegex = /(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?\/[a-zA-Z0-9]{2,}|((https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?/gi
export type IThoughtProps = {
list: ThoughtItem[]
isThinking?: boolean
......
......@@ -128,8 +128,12 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
<div className='max-w-[200px] text-[13px] font-medium text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{item.name}</div>
</div>
<div className='max-w-[140px] flex text-xs text-gray-500 overflow-hidden text-ellipsis whitespace-nowrap'>
{formatNumber(item.word_count)} {t('appDebug.feature.dataSet.words')} · {formatNumber(item.document_count)} {t('appDebug.feature.dataSet.textBlocks')}
<div className='flex text-xs text-gray-500 overflow-hidden whitespace-nowrap'>
<span className='max-w-[100px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.word_count)}</span>
{t('appDebug.feature.dataSet.words')}
<span className='px-0.5'>·</span>
<span className='max-w-[100px] min-w-[8px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.document_count)} </span>
{t('appDebug.feature.dataSet.textBlocks')}
</div>
</div>
))}
......
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3332 4L5.99984 11.3333L2.6665 8" stroke="#155EEF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.3332 4L5.99984 11.3333L2.6665 8",
"stroke": "#155EEF",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Checked"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Checked.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
export default Icon
export { default as Checked } from './Checked'
import ReactMarkdown from "react-markdown";
import "katex/dist/katex.min.css";
import RemarkMath from "remark-math";
import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import ReactMarkdown from 'react-markdown'
import 'katex/dist/katex.min.css'
import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RemarkGfm from 'remark-gfm'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { useRef, useState, RefObject, useEffect } from "react";
import type { RefObject } from 'react'
import { useEffect, useRef, useState } from 'react'
// import { copyToClipboard } from "../utils";
// https://txtfiddle.com/~hlshwya/extract-urls-from-text
const urlRegex = /\b((https?|ftp|file):\/\/|(www|ftp)\.)[-A-Z0-9+&@#\/%?=~_|$!:,.;]*[A-Z0-9+&@#\/%=~_|$]/ig
function highlightURL(content: string) {
return content.replace(urlRegex, '[$&]($&)')
}
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
const ref = useRef<HTMLPreElement>(null)
return (
<pre ref={ref}>
......@@ -18,38 +24,37 @@ export function PreCode(props: { children: any }) {
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
const code = ref.current.innerText
// copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
);
)
}
const useLazyLoad = (ref: RefObject<Element>): boolean => {
const [isIntersecting, setIntersecting] = useState<boolean>(false);
const [isIntersecting, setIntersecting] = useState<boolean>(false)
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIntersecting(true);
observer.disconnect();
setIntersecting(true)
observer.disconnect()
}
});
})
if (ref.current) {
observer.observe(ref.current);
}
if (ref.current)
observer.observe(ref.current)
return () => {
observer.disconnect();
};
}, [ref]);
observer.disconnect()
}
}, [ref])
return isIntersecting;
};
return isIntersecting
}
export function Markdown(props: { content: string }) {
return (
......@@ -62,7 +67,8 @@ export function Markdown(props: { content: string }) {
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
return (!inline && match)
? (
<SyntaxHighlighter
{...props}
children={String(children).replace(/\n$/, '')}
......@@ -71,17 +77,19 @@ export function Markdown(props: { content: string }) {
showLineNumbers
PreTag="div"
/>
) : (
)
: (
<code {...props} className={className}>
{children}
</code>
)
}
},
}}
linkTarget={"_blank"}
linkTarget={'_blank'}
>
{props.content}
{/* Markdown detect has problem. */}
{highlightURL(props.content)}
</ReactMarkdown>
</div>
);
)
}
......@@ -7,7 +7,7 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
import { UNIVERSAL_CHAT_MODEL_LIST as MODEL_LIST } from '@/config'
import { Checked as CheckedIcon } from '@/app/components/base/icons/src/public/model'
export type IModelConfigProps = {
modelId: string
onChange?: (model: string) => void
......@@ -32,17 +32,24 @@ const ModelConfig: FC<IModelConfigProps> = ({
<div className='flex items-center justify-between h-[52px] px-3 rounded-xl bg-gray-50'>
<div className='text-sm font-semibold text-gray-800'>{t('explore.universalChat.model')}</div>
<div className="relative z-10">
<div ref={triggerRef} onClick={() => !readonly && toogleOption()} className={cn(readonly ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 ')}>
<div
ref={triggerRef}
onClick={() => !readonly && toogleOption()}
className={cn(
readonly ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg',
isShowOption && 'bg-gray-100',
)}>
<ModelIcon modelId={currModel?.id as string} />
<div className="text-sm gray-900">{currModel?.name}</div>
{!readonly && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
</div>
{isShowOption && (
<div className={cn('min-w-[159px] absolute right-0 bg-gray-50 rounded-lg shadow')}>
<div className={cn('absolute top-10 right-0 bg-white rounded-lg shadow')}>
{MODEL_LIST.map(item => (
<div key={item.id} onClick={() => onChange?.(item.id)} className="flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100">
<div key={item.id} onClick={() => onChange?.(item.id)} className="w-[232px] flex items-center h-9 px-4 rounded-lg cursor-pointer hover:bg-gray-100">
<ModelIcon className='shrink-0 mr-2' modelId={item?.id} />
<div className="text-sm gray-900 whitespace-nowrap">{item.name}</div>
{(item.id === currModel?.id) && <CheckedIcon className='absolute right-4' />}
</div>
))}
</div>
......
......@@ -78,7 +78,7 @@ const Plugins: FC<IPluginsProps> = ({
>
{isLoading
? (
<div className='flex items-center h-[200px]'>
<div className='flex items-center h-[166px]'>
<Loading type='area' />
</div>
)
......
......@@ -40,7 +40,7 @@ import ConfigSummary from '@/app/components/explore/universal-chat/config-view/s
import ConfigDetail from '@/app/components/explore/universal-chat/config-view/detail'
import { fetchDatasets } from '@/service/datasets'
const APP_ID = 'universal-chat'
const DEFAULT_MODEL_ID = 'claude-2' // gpt-4, claude-2
const DEFAULT_MODEL_ID = 'gpt-3.5-turbo' // gpt-4, claude-2
const DEFAULT_PLUGIN = {
google_search: false,
web_reader: true,
......@@ -235,11 +235,14 @@ const Main: FC<IMainProps> = () => {
})
})
setChatList(newChatList)
setErrorHappened(false)
})
}
if (isNewConversation)
if (isNewConversation) {
setChatList(generateNewChatListWithOpenstatement())
setErrorHappened(false)
}
setControlFocus(Date.now())
}
......@@ -405,7 +408,7 @@ const Main: FC<IMainProps> = () => {
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
const [messageTaskId, setMessageTaskId] = useState('')
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
const [errorHappened, setErrorHappened] = useState(false)
const handleSend = async (message: string) => {
if (isResponsing) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
......@@ -462,6 +465,7 @@ const Main: FC<IMainProps> = () => {
setHasStopResponded(false)
setResponsingTrue()
setErrorHappened(false)
setIsShowSuggestion(false)
sendChatMessage(data, {
......@@ -509,8 +513,8 @@ const Main: FC<IMainProps> = () => {
if (thought.thought === '[DONE]')
return
responseItem.id = thought.message_id;
// thought finished then start to return message
(responseItem as any).agent_thoughts.push(thought)
// thought finished then start to return message. Warning: use push agent_thoughts.push would caused problem when the thought is more then 2
(responseItem as any).agent_thoughts = [...(responseItem as any).agent_thoughts, thought] // .push(thought)
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
......@@ -521,7 +525,9 @@ const Main: FC<IMainProps> = () => {
setChatList(newListWithAnswer)
},
onError() {
setErrorHappened(true)
setResponsingFalse()
// role back placeholder answer
setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
......@@ -625,7 +631,7 @@ const Main: FC<IMainProps> = () => {
)
}>
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] mb-3.5 overflow-hidden')}>
{(!isNewConversation || isResponsing) && (
{(!isNewConversation || isResponsing || errorHappened) && (
<div className='absolute z-10 top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'>
<div className='text-gray-900'>{conversationName}</div>
<div className='flex items-center shrink-0 ml-2 space-x-2'>
......@@ -637,7 +643,7 @@ const Main: FC<IMainProps> = () => {
)}
<div className={cn((!isNewConversation || isResponsing) && 'pt-[90px]', 'pc:w-[794px] max-w-full mobile:w-full mx-auto h-full overflow-y-auto')} ref={chatListDomRef}>
<Chat
isShowConfigElem={isNewConversation && !chatList.some(item => item.isAnswer)}
isShowConfigElem={isNewConversation && chatList.length === 0}
configElem={<Init
modelId={modelId}
onModelChange={setModeId}
......
......@@ -79,13 +79,13 @@ const Sidebar: FC<ISidebarProps> = ({
checkHasPinned()
}, [controlUpdateList])
const maxListHeight = isInstalledApp ? 'max-h-[30vh]' : 'max-h-[40vh]'
const maxListHeight = (isInstalledApp || isUniversalChat) ? 'max-h-[30vh]' : 'max-h-[40vh]'
return (
<div
className={
cn(
isInstalledApp ? 'tablet:h-[calc(100vh_-_74px)]' : 'tablet:h-[calc(100vh_-_3rem)]',
(isInstalledApp || isUniversalChat) ? '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',
)
}
......
......@@ -75,7 +75,7 @@ const List: FC<IListProps> = ({
return (
<nav
ref={listRef}
className={cn(className, 'shrink-0 space-y-1 bg-white pb-[85px] overflow-y-auto')}
className={cn(className, 'shrink-0 space-y-1 bg-white overflow-y-auto')}
>
{list.map((item) => {
const isCurrent = item.id === currentId
......
......@@ -65,7 +65,7 @@ const List: FC<IListProps> = ({
return (
<nav
ref={listRef}
className={cn(className, 'shrink-0 space-y-1 bg-white pb-[85px] overflow-y-auto')}
className={cn(className, 'shrink-0 space-y-1 bg-white overflow-y-auto')}
>
{list.map((item) => {
const isCurrent = item.id === currentId
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment