Commit b5695337 authored by StyleZhang's avatar StyleZhang

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

parents 10376aba e828217c
......@@ -6,6 +6,7 @@ import GA, { GaType } from '@/app/components/base/ga'
import HeaderWrapper from '@/app/components/header/HeaderWrapper'
import Header from '@/app/components/header'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
const Layout = ({ children }: { children: ReactNode }) => {
return (
......@@ -14,10 +15,12 @@ const Layout = ({ children }: { children: ReactNode }) => {
<SwrInitor>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
{children}
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</SwrInitor>
......
......@@ -44,9 +44,10 @@ export type IAnswerProps = {
isResponsing?: boolean
answerIconClassName?: string
thoughts?: ThoughtItem[]
isThinking?: boolean
}
// The component needs to maintain its own state to control whether to display input component
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing, answerIconClassName, thoughts }) => {
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing, answerIconClassName, thoughts, isThinking }) => {
const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
const [showEdit, setShowEdit] = useState(false)
const [loading, setLoading] = useState(false)
......@@ -204,7 +205,7 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
: (
<div>
{(thoughts && thoughts.length > 0) && (
<Thought list={thoughts || []}/>
<Thought list={thoughts || []} isThinking={isThinking} />
)}
<Markdown content={content} />
</div>
......
......@@ -47,6 +47,7 @@ export type IChatProps = {
isShowSpeechToText?: boolean
answerIconClassName?: string
isShowConfigElem?: boolean
isThoughting?: boolean
}
const Chat: FC<IChatProps> = ({
......@@ -156,6 +157,7 @@ const Chat: FC<IChatProps> = ({
if (item.isAnswer) {
const isLast = item.id === chatList[chatList.length - 1].id
const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]')
const isThinking = item.agent_thoughts && item.agent_thoughts?.length > 0 && !item.agent_thoughts.find(item => item.thought !== '[DONE]')
return <Answer
key={item.id}
item={item}
......@@ -167,6 +169,7 @@ const Chat: FC<IChatProps> = ({
isResponsing={isResponsing && isLast}
answerIconClassName={answerIconClassName}
thoughts={thoughts}
isThinking={isThinking}
/>
}
return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
......
......@@ -5,13 +5,14 @@ import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import type { ThoughtItem } from '../type'
import s from './style.module.css'
import { DataSet, Search, ThoughtList, WebReader } from '@/app/components/base/icons/src/public/thought'
import { DataSet, Loading as LodingIcon, Search, ThoughtList, WebReader } from '@/app/components/base/icons/src/public/thought'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
// 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
}
const getIcon = (toolId: string) => {
......@@ -27,6 +28,7 @@ const getIcon = (toolId: string) => {
const Thought: FC<IThoughtProps> = ({
list,
isThinking,
}) => {
const { t } = useTranslation()
const [isShowDetail, setIsShowDetail] = React.useState(false)
......@@ -62,8 +64,11 @@ const Thought: FC<IThoughtProps> = ({
return (
<div className={cn(s.wrap, !isShowDetail && s.wrapHoverEffect, 'inline-block mb-2 px-2 py-0.5 rounded-md text-xs text-gray-500 font-medium')} >
<div className='flex items-center h-6 space-x-1 cursor-pointer' onClick={() => setIsShowDetail(!isShowDetail)} >
<ThoughtList />
<div>{t(`explore.universalChat.thought.${isShowDetail ? 'hide' : 'show'}`)}{t('explore.universalChat.thought.processOfThought')}</div>
{!isThinking ? <ThoughtList /> : <div className='animate-spin'><LodingIcon /></div>}
<div dangerouslySetInnerHTML= {{
__html: isThinking ? getThoughtText(list[0]) : (t(`explore.universalChat.thought.${isShowDetail ? 'hide' : 'show'}`) + t('explore.universalChat.thought.processOfThought')),
}}
></div>
<ChevronDown className={isShowDetail ? 'rotate-180' : '' } />
</div>
{isShowDetail && (
......
......@@ -21,6 +21,7 @@ export type ThoughtItem = {
tool: string // plugin or dataset
thought: string
tool_input: string
message_id: string
}
export type IChatItem = {
id: string
......
......@@ -4,7 +4,6 @@ import React from 'react'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import useSWR from 'swr'
import DatasetConfig from '../dataset-config'
import ChatGroup from '../features/chat-group'
import ExperienceEnchanceGroup from '../features/experience-enchance-group'
......@@ -20,7 +19,7 @@ import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import ConfigVar from '@/app/components/app/configuration/config-var'
import type { PromptVariable } from '@/models/debug'
import { AppType } from '@/types/app'
import { fetchTenantInfo } from '@/service/common'
import { useProviderContext } from '@/context/provider-context'
const Config: FC = () => {
const {
......@@ -39,8 +38,7 @@ const Config: FC = () => {
setSpeechToTextConfig,
} = useContext(ConfigContext)
const isChatApp = mode === AppType.chat
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
const openaiProvider = userInfo?.providers?.find(({ token_is_set, is_valid, provider_name }) => token_is_set && is_valid && provider_name === 'openai')
const { currentProvider } = useProviderContext()
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
......@@ -92,7 +90,7 @@ const Config: FC = () => {
},
})
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && openaiProvider))
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && currentProvider?.provider_name === 'openai'))
const hasToolbox = false
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
......@@ -122,7 +120,7 @@ const Config: FC = () => {
isChatApp={isChatApp}
config={featureConfig}
onChange={handleFeatureChange}
showSpeechToTextItem={!!openaiProvider}
showSpeechToTextItem={currentProvider?.provider_name === 'openai'}
/>
)}
{showAutomatic && (
......@@ -162,7 +160,7 @@ const Config: FC = () => {
}
}
isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
isShowSpeechText={featureConfig.speechToText}
isShowSpeechText={featureConfig.speechToText && currentProvider?.provider_name === 'openai'}
/>
)
}
......
......@@ -22,6 +22,7 @@ import type { ModelConfig as BackendModelConfig } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config'
import { useProviderContext } from '@/context/provider-context'
type IDebug = {
hasSetAPIKEY: boolean
......@@ -51,7 +52,7 @@ const Debug: FC<IDebug> = ({
modelConfig,
completionParams,
} = useContext(ConfigContext)
const { currentProvider } = useProviderContext()
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
const chatListDomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
......@@ -389,7 +390,7 @@ const Debug: FC<IDebug> = ({
}}
isShowSuggestion={doShowSuggestion}
suggestionList={suggestQuestions}
isShowSpeechToText={speechToTextConfig.enabled}
isShowSpeechToText={speechToTextConfig.enabled && currentProvider?.provider_name === 'openai'}
/>
</div>
</div>
......
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7998_4025)">
<path d="M6 1.125V2.375M6 9V11M2.875 6H1.125M10.625 6H9.875M9.22855 9.22855L8.875 8.875M9.33211 2.70789L8.625 3.415M2.46079 9.53921L3.875 8.125M2.56434 2.60434L3.625 3.665" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_7998_4025">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "12",
"height": "12",
"viewBox": "0 0 12 12",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#clip0_7998_4025)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6 1.125V2.375M6 9V11M2.875 6H1.125M10.625 6H9.875M9.22855 9.22855L8.875 8.875M9.33211 2.70789L8.625 3.415M2.46079 9.53921L3.875 8.125M2.56434 2.60434L3.625 3.665",
"stroke": "#667085",
"stroke-width": "1.25",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_7998_4025"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "12",
"height": "12",
"fill": "white"
},
"children": []
}
]
}
]
}
]
},
"name": "Loading"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Loading.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 DataSet } from './DataSet'
export { default as Loading } from './Loading'
export { default as Search } from './Search'
export { default as ThoughtList } from './ThoughtList'
export { default as WebReader } from './WebReader'
......@@ -4,6 +4,7 @@ import React from 'react'
import cn from 'classnames'
import { useBoolean, useClickAway } from 'ahooks'
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'
......@@ -18,6 +19,8 @@ const ModelConfig: FC<IModelConfigProps> = ({
onChange,
readonly,
}) => {
const { t } = useTranslation()
const currModel = MODEL_LIST.find(item => item.id === modelId)
const [isShowOption, { setFalse: hideOption, toggle: toogleOption }] = useBoolean(false)
const triggerRef = React.useRef(null)
......@@ -27,7 +30,7 @@ const ModelConfig: FC<IModelConfigProps> = ({
return (
<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'>Model</div>
<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 ')}>
<ModelIcon modelId={currModel?.id as string} />
......
......@@ -480,7 +480,7 @@ const Main: FC<IMainProps> = () => {
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...questionItem } as any)
draft.push({ ...responseItem })
})
......@@ -506,9 +506,20 @@ const Main: FC<IMainProps> = () => {
}
},
onThought(thought) {
// debugger
// thought then start to return message
if (thought.thought === '[DONE]')
return
responseItem.id = thought.message_id;
// thought finished then start to return message
(responseItem as any).agent_thoughts.push(thought)
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
// console.log('start render thought')
setChatList(newListWithAnswer)
},
onError() {
setResponsingFalse()
......
'use client'
import { createContext, useContext } from 'use-context-selector'
import useSWR from 'swr'
import { fetchTenantInfo } from '@/service/common'
const ProviderContext = createContext<{ currentProvider: {
provider: string
provider_name: string
token_is_set: boolean
is_valid: boolean
token_is_valid: boolean
} | null | undefined }>({
currentProvider: null,
})
export const useProviderContext = () => useContext(ProviderContext)
type ProviderContextProviderProps = {
children: React.ReactNode
}
export const ProviderContextProvider = ({
children,
}: ProviderContextProviderProps) => {
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
const currentProvider = userInfo?.providers?.find(({ token_is_set, is_valid }) => token_is_set && is_valid)
return (
<ProviderContext.Provider value={{ currentProvider }}>
{children}
</ProviderContext.Provider>
)
}
export default ProviderContext
......@@ -38,6 +38,7 @@ const translation = {
universalChat: {
welcome: 'Start chat with Dify',
welcomeDescribe: 'Your AI conversation companion for personalized assistance',
model: 'Model',
plugins: {
name: 'Plugins',
google_search: {
......
......@@ -2,7 +2,7 @@ const translation = {
title: '我的应用',
sidebar: {
discovery: '发现',
chat: '聊天',
chat: '智聊',
workspace: '工作区',
action: {
pin: '置顶',
......@@ -38,7 +38,9 @@ const translation = {
universalChat: {
welcome: '开始和 Dify 聊天吧',
welcomeDescribe: '您的 AI 对话伴侣,为您提供个性化的帮助',
model: '模型',
plugins: {
name: '插件',
google_search: {
name: '谷歌搜索',
more: {
......
/* eslint-disable no-new, prefer-promise-reject-errors */
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
import Toast from '@/app/components/base/toast'
import type { ThoughtItem } from '@/app/components/app/chat/type'
const TIME_OUT = 100000
......@@ -30,7 +31,7 @@ export type IOnDataMoreInfo = {
}
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
export type IOnThought = (though: { id: string; tool: string; thought: string }) => void
export type IOnThought = (though: ThoughtItem) => void
export type IOnCompleted = (hasError?: boolean) => void
export type IOnError = (msg: string) => void
......
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