mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
Merge branch 'main' into tp
This commit is contained in:
@ -51,5 +51,32 @@ if $web_modified; then
|
||||
echo "Running ESLint on web module"
|
||||
cd ./web || exit 1
|
||||
npx lint-staged
|
||||
|
||||
echo "Running unit tests check"
|
||||
modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
|
||||
|
||||
if [ -n "$modified_files" ]; then
|
||||
for file in $modified_files; do
|
||||
test_file="${file%.*}.spec.ts"
|
||||
echo "Checking for test file: $test_file"
|
||||
|
||||
# check if the test file exists
|
||||
if [ -f "../$test_file" ]; then
|
||||
echo "Detected changes in $file, running corresponding unit tests..."
|
||||
npm run test "../$test_file"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Unit tests failed. Please fix the errors before committing."
|
||||
exit 1
|
||||
fi
|
||||
echo "Unit tests for $file passed."
|
||||
else
|
||||
echo "Warning: $file does not have a corresponding test file."
|
||||
fi
|
||||
|
||||
done
|
||||
echo "All unit tests for modified web/utils files have passed."
|
||||
fi
|
||||
|
||||
cd ../
|
||||
fi
|
||||
|
||||
@ -18,6 +18,10 @@ yarn install --frozen-lockfile
|
||||
|
||||
Then, configure the environment variables. Create a file named `.env.local` in the current directory and copy the contents from `.env.example`. Modify the values of these environment variables according to your requirements:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
```
|
||||
# For production release, change this to PRODUCTION
|
||||
NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT
|
||||
@ -78,7 +82,7 @@ If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscod
|
||||
|
||||
We start to use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
|
||||
|
||||
You can create a test file with a suffix of `.spec` beside the file that to be tested. For example, if you want to test a file named `util.ts`. The test file name should be `util.spec.ts`.
|
||||
You can create a test file with a suffix of `.spec` beside the file that to be tested. For example, if you want to test a file named `util.ts`. The test file name should be `util.spec.ts`.
|
||||
|
||||
Run test:
|
||||
|
||||
|
||||
@ -143,6 +143,7 @@ const ActivateForm = () => {
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('login.namePlaceholder') || ''}
|
||||
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -159,6 +160,7 @@ const ActivateForm = () => {
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
|
||||
tabIndex={2}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div>
|
||||
|
||||
@ -263,7 +263,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
<div>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -46,6 +46,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
||||
const config = useConfigFromDebugContext()
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
isResponding,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
@ -80,6 +81,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
||||
query: message,
|
||||
inputs,
|
||||
model_config: configData,
|
||||
parent_message_id: chatListRef.current.at(-1)?.id || null,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files?.length && supportVision)
|
||||
@ -93,7 +95,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled])
|
||||
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import { useChat } from '@/app/components/base/chat/chat/hooks'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import type { OnSend } from '@/app/components/base/chat/types'
|
||||
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConversationMessages,
|
||||
@ -45,10 +45,12 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
||||
const config = useConfigFromDebugContext()
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
isResponding,
|
||||
handleSend,
|
||||
suggestedQuestions,
|
||||
handleStop,
|
||||
handleUpdateChatList,
|
||||
handleRestart,
|
||||
handleAnnotationAdded,
|
||||
handleAnnotationEdited,
|
||||
@ -64,7 +66,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
||||
)
|
||||
useFormattingChangedSubscription(chatList)
|
||||
|
||||
const doSend: OnSend = useCallback((message, files) => {
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
if (checkCanSend && !checkCanSend())
|
||||
return
|
||||
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
|
||||
@ -81,10 +83,17 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
||||
},
|
||||
}
|
||||
|
||||
const lastAnswer = chatListRef.current.at(-1)
|
||||
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs,
|
||||
model_config: configData,
|
||||
parent_message_id: last_answer?.id || (lastAnswer
|
||||
? lastAnswer.isOpeningStatement
|
||||
? null
|
||||
: lastAnswer.id
|
||||
: null),
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files?.length && supportVision)
|
||||
@ -98,7 +107,23 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
}, [appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
|
||||
}, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
const allToolIcons = useMemo(() => {
|
||||
const icons: Record<string, any> = {}
|
||||
@ -123,6 +148,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
||||
chatFooterClassName='px-6 pt-10 pb-4'
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
showPromptLog
|
||||
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
||||
|
||||
@ -16,6 +16,7 @@ import timezone from 'dayjs/plugin/timezone'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { UUID_NIL } from '../../base/chat/constants'
|
||||
import s from './style.module.css'
|
||||
import VarPanel from './var-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
@ -81,72 +82,92 @@ const PARAM_MAP = {
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
}
|
||||
|
||||
// Format interface data for easy display
|
||||
function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedbacks.find((item: any) => item.from_source === 'user'), // user feedback
|
||||
adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
|
||||
feedbackDisabled: false,
|
||||
isAnswer: true,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
log: [
|
||||
...item.message,
|
||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: item.answer,
|
||||
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
conversationId,
|
||||
input: {
|
||||
inputs: item.inputs,
|
||||
query: item.query,
|
||||
},
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
citation: item.metadata?.retriever_resources,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: item.annotation.id,
|
||||
authorName: item.annotation.account.name,
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})(),
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
parentMessageId: item.parent_message_id || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
|
||||
adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
|
||||
feedbackDisabled: false,
|
||||
isAnswer: true,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
log: [
|
||||
...item.message,
|
||||
...(item.message[item.message.length - 1]?.role !== 'assistant'
|
||||
? [
|
||||
{
|
||||
role: 'assistant',
|
||||
text: item.answer,
|
||||
files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
conversationId,
|
||||
input: {
|
||||
inputs: item.inputs,
|
||||
query: item.query,
|
||||
},
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).tz(timezone).format(format),
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
citation: item.metadata?.retriever_resources,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
}
|
||||
}
|
||||
let nextMessageId = null
|
||||
for (const item of messages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||
break
|
||||
}
|
||||
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: item.annotation.id,
|
||||
authorName: item.annotation.account.name,
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})(),
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(newChatList, item, conversationId, timezone, format)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return newChatList.reverse()
|
||||
}
|
||||
|
||||
// const displayedParams = CompletionParams.slice(0, -2)
|
||||
@ -171,6 +192,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
})))
|
||||
const { t } = useTranslation()
|
||||
const [items, setItems] = React.useState<IChatItem[]>([])
|
||||
const fetchedMessages = useRef<ChatMessage[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||
const fetchData = async () => {
|
||||
@ -192,7 +214,8 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
const varValues = messageRes.data[0].inputs
|
||||
setVarValues(varValues)
|
||||
}
|
||||
const newItems = [...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string), ...items]
|
||||
fetchedMessages.current = [...fetchedMessages.current, ...messageRes.data]
|
||||
const newItems = getFormattedChatList(fetchedMessages.current, detail.id, timezone!, t('appLog.dateTimeFormat') as string)
|
||||
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
newItems.unshift({
|
||||
id: 'introduction',
|
||||
@ -435,7 +458,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: items.length < 8
|
||||
: (items.length < 8 && !hasMore)
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
|
||||
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@ -44,6 +45,8 @@ const ChatWrapper = () => {
|
||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
@ -63,11 +66,18 @@ const ChatWrapper = () => {
|
||||
currentChatInstanceRef.current.handleStop = handleStop
|
||||
}, [])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files) => {
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const lastAnswer = chatListRef.current.at(-1)
|
||||
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || (lastAnswer
|
||||
? lastAnswer.isOpeningStatement
|
||||
? null
|
||||
: lastAnswer.id
|
||||
: null),
|
||||
}
|
||||
|
||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||
@ -83,6 +93,7 @@ const ChatWrapper = () => {
|
||||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
appConfig,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
@ -92,6 +103,23 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
@ -148,6 +176,7 @@ const ChatWrapper = () => {
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
|
||||
@ -12,10 +12,10 @@ import produce from 'immer'
|
||||
import type {
|
||||
Callback,
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { getPrevChatList } from '../utils'
|
||||
import {
|
||||
delConversation,
|
||||
fetchAppInfo,
|
||||
@ -34,7 +34,6 @@ import type {
|
||||
AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
@ -108,32 +107,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||
|
||||
const appPrevChatList = useMemo(() => {
|
||||
const data = appChatListData?.data || []
|
||||
const chatList: ChatItem[] = []
|
||||
|
||||
if (currentConversationId && data.length) {
|
||||
data.forEach((item: any) => {
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return chatList
|
||||
}, [appChatListData, currentConversationId])
|
||||
const appPrevChatList = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? getPrevChatList(appChatListData.data)
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ type AnswerProps = {
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
appData?: AppData
|
||||
noChatInput?: boolean
|
||||
}
|
||||
const Answer: FC<AnswerProps> = ({
|
||||
item,
|
||||
@ -48,6 +49,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
appData,
|
||||
noChatInput,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@ -110,6 +112,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
question={question}
|
||||
index={index}
|
||||
showPromptLog={showPromptLog}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import RegenerateBtn from '@/app/components/base/regenerate-btn'
|
||||
import cn from '@/utils/classnames'
|
||||
import CopyBtn from '@/app/components/base/copy-btn'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
@ -28,6 +29,7 @@ type OperationProps = {
|
||||
maxSize: number
|
||||
contentWidth: number
|
||||
hasWorkflowProcess: boolean
|
||||
noChatInput?: boolean
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
item,
|
||||
@ -37,6 +39,7 @@ const Operation: FC<OperationProps> = ({
|
||||
maxSize,
|
||||
contentWidth,
|
||||
hasWorkflowProcess,
|
||||
noChatInput,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@ -45,6 +48,7 @@ const Operation: FC<OperationProps> = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
onFeedback,
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const {
|
||||
@ -159,12 +163,13 @@ const Operation: FC<OperationProps> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
|
||||
}
|
||||
{
|
||||
config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
|
||||
<div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.operation.agree')}
|
||||
>
|
||||
<div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
|
||||
<Tooltip popupContent={t('appDebug.operation.agree')}>
|
||||
<div
|
||||
className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
|
||||
onClick={() => handleFeedback('like')}
|
||||
|
||||
@ -12,6 +12,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'answerIcon'
|
||||
| 'allToolIcons'
|
||||
| 'onSend'
|
||||
| 'onRegenerate'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
@ -36,6 +37,7 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
@ -51,6 +53,7 @@ export const ChatContextProvider = ({
|
||||
answerIcon,
|
||||
allToolIcons,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
|
||||
@ -647,7 +647,8 @@ export const useChat = (
|
||||
|
||||
return {
|
||||
chatList,
|
||||
setChatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
conversationId: conversationId.current,
|
||||
isResponding,
|
||||
setIsResponding,
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
OnRegenerate,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import type { ThemeBuilder } from '../embedded-chatbot/theme/theme-context'
|
||||
@ -42,6 +43,7 @@ export type ChatProps = {
|
||||
onStopResponding?: () => void
|
||||
noChatInput?: boolean
|
||||
onSend?: OnSend
|
||||
onRegenerate?: OnRegenerate
|
||||
chatContainerClassName?: string
|
||||
chatContainerInnerClassName?: string
|
||||
chatFooterClassName?: string
|
||||
@ -67,6 +69,7 @@ const Chat: FC<ChatProps> = ({
|
||||
appData,
|
||||
config,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
chatList,
|
||||
isResponding,
|
||||
noStopResponding,
|
||||
@ -186,6 +189,7 @@ const Chat: FC<ChatProps> = ({
|
||||
answerIcon={answerIcon}
|
||||
allToolIcons={allToolIcons}
|
||||
onSend={onSend}
|
||||
onRegenerate={onRegenerate}
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
@ -219,6 +223,7 @@ const Chat: FC<ChatProps> = ({
|
||||
showPromptLog={showPromptLog}
|
||||
chatAnswerContainerInner={chatAnswerContainerInner}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
noChatInput={noChatInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -95,6 +95,7 @@ export type IChatItem = {
|
||||
// for agent log
|
||||
conversationId?: string
|
||||
input?: any
|
||||
parentMessageId?: string
|
||||
}
|
||||
|
||||
export type Metadata = {
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
||||
export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react'
|
||||
import Chat from '../chat'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
OnSend,
|
||||
} from '../types'
|
||||
import { useChat } from '../chat/hooks'
|
||||
@ -45,11 +46,13 @@ const ChatWrapper = () => {
|
||||
} as ChatConfig
|
||||
}, [appParams, currentConversationItem?.introduction, currentConversationId])
|
||||
const {
|
||||
chatListRef,
|
||||
chatList,
|
||||
handleSend,
|
||||
handleStop,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
handleUpdateChatList,
|
||||
} = useChat(
|
||||
appConfig,
|
||||
{
|
||||
@ -65,11 +68,18 @@ const ChatWrapper = () => {
|
||||
currentChatInstanceRef.current.handleStop = handleStop
|
||||
}, [])
|
||||
|
||||
const doSend: OnSend = useCallback((message, files) => {
|
||||
const doSend: OnSend = useCallback((message, files, last_answer) => {
|
||||
const lastAnswer = chatListRef.current.at(-1)
|
||||
|
||||
const data: any = {
|
||||
query: message,
|
||||
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: last_answer?.id || (lastAnswer
|
||||
? lastAnswer.isOpeningStatement
|
||||
? null
|
||||
: lastAnswer.id
|
||||
: null),
|
||||
}
|
||||
|
||||
if (appConfig?.file_upload?.image.enabled && files?.length)
|
||||
@ -85,6 +95,7 @@ const ChatWrapper = () => {
|
||||
},
|
||||
)
|
||||
}, [
|
||||
chatListRef,
|
||||
appConfig,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
@ -94,6 +105,23 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (inputsForms.length) {
|
||||
return (
|
||||
@ -136,6 +164,7 @@ const ChatWrapper = () => {
|
||||
chatFooterClassName='pb-4'
|
||||
chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={chatNode}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
|
||||
@ -11,10 +11,10 @@ import { useLocalStorageState } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
Feedback,
|
||||
} from '../types'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils'
|
||||
import {
|
||||
fetchAppInfo,
|
||||
fetchAppMeta,
|
||||
@ -28,10 +28,8 @@ import type {
|
||||
// AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import { getProcessedInputsFromUrlParams } from '@/app/components/base/chat/utils'
|
||||
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
@ -75,32 +73,12 @@ export const useEmbeddedChatbot = () => {
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||
|
||||
const appPrevChatList = useMemo(() => {
|
||||
const data = appChatListData?.data || []
|
||||
const chatList: ChatItem[] = []
|
||||
|
||||
if (currentConversationId && data.length) {
|
||||
data.forEach((item: any) => {
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return chatList
|
||||
}, [appChatListData, currentConversationId])
|
||||
const appPrevChatList = useMemo(
|
||||
() => (currentConversationId && appChatListData?.data.length)
|
||||
? getPrevChatList(appChatListData.data)
|
||||
: [],
|
||||
[appChatListData, currentConversationId],
|
||||
)
|
||||
|
||||
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
|
||||
|
||||
@ -155,7 +133,7 @@ export const useEmbeddedChatbot = () => {
|
||||
type: 'text-input',
|
||||
}
|
||||
})
|
||||
}, [appParams])
|
||||
}, [initInputs, appParams])
|
||||
|
||||
useEffect(() => {
|
||||
// init inputs from url params
|
||||
|
||||
@ -63,7 +63,9 @@ export type ChatItem = IChatItem & {
|
||||
conversationId?: string
|
||||
}
|
||||
|
||||
export type OnSend = (message: string, files?: VisionFile[]) => void
|
||||
export type OnSend = (message: string, files?: VisionFile[], last_answer?: ChatItem) => void
|
||||
|
||||
export type OnRegenerate = (chatItem: ChatItem) => void
|
||||
|
||||
export type Callback = {
|
||||
onSuccess: () => void
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||
import { UUID_NIL } from './constants'
|
||||
import type { ChatItem } from './types'
|
||||
|
||||
async function decodeBase64AndDecompress(base64String: string) {
|
||||
const binaryString = atob(base64String)
|
||||
const compressedUint8Array = Uint8Array.from(binaryString, char => char.charCodeAt(0))
|
||||
const decompressedStream = new Response(compressedUint8Array).body.pipeThrough(new DecompressionStream('gzip'))
|
||||
const decompressedStream = new Response(compressedUint8Array).body?.pipeThrough(new DecompressionStream('gzip'))
|
||||
const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer()
|
||||
return new TextDecoder().decode(decompressedArrayBuffer)
|
||||
}
|
||||
@ -15,6 +19,57 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
||||
return inputs
|
||||
}
|
||||
|
||||
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||
// we append answer first and then question since will reverse the whole chatList later
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
})
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the latest thread messages from all messages of the conversation.
|
||||
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||
*
|
||||
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||
* @returns An array of ChatItems representing the latest thread.
|
||||
*/
|
||||
function getPrevChatList(fetchedMessages: any[]) {
|
||||
const ret: ChatItem[] = []
|
||||
let nextMessageId = null
|
||||
|
||||
for (const item of fetchedMessages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(ret, item)
|
||||
break
|
||||
}
|
||||
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret.reverse()
|
||||
}
|
||||
|
||||
export {
|
||||
getProcessedInputsFromUrlParams,
|
||||
getPrevChatList,
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 524 B |
@ -0,0 +1,23 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"xmlns": "http://www.w3.org/2000/svg",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Refresh"
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Refresh.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} />)
|
||||
|
||||
Icon.displayName = 'Refresh'
|
||||
|
||||
export default Icon
|
||||
@ -18,6 +18,7 @@ export { default as Menu01 } from './Menu01'
|
||||
export { default as Pin01 } from './Pin01'
|
||||
export { default as Pin02 } from './Pin02'
|
||||
export { default as Plus02 } from './Plus02'
|
||||
export { default as Refresh } from './Refresh'
|
||||
export { default as Settings01 } from './Settings01'
|
||||
export { default as Settings04 } from './Settings04'
|
||||
export { default as Target04 } from './Target04'
|
||||
|
||||
31
web/app/components/base/regenerate-btn/index.tsx
Normal file
31
web/app/components/base/regenerate-btn/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import { t } from 'i18next'
|
||||
import { Refresh } from '../icons/src/vender/line/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const RegenerateBtn = ({ className, onClick }: Props) => {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Tooltip
|
||||
popupContent={t('appApi.regenerate') as string}
|
||||
>
|
||||
<div
|
||||
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
|
||||
onClick={() => onClick?.()}
|
||||
style={{
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}}
|
||||
>
|
||||
<Refresh className="p-[3.5px] w-6 h-6 text-[#667085] hover:bg-gray-50" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegenerateBtn
|
||||
18
web/app/components/datasets/create/step-two/escape.ts
Normal file
18
web/app/components/datasets/create/step-two/escape.ts
Normal file
@ -0,0 +1,18 @@
|
||||
function escape(input: string): string {
|
||||
if (!input || typeof input !== 'string')
|
||||
return ''
|
||||
|
||||
const res = input
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('\0', '\\0')
|
||||
.replaceAll('\b', '\\b')
|
||||
.replaceAll('\f', '\\f')
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll('\t', '\\t')
|
||||
.replaceAll('\v', '\\v')
|
||||
.replaceAll('\'', '\\\'')
|
||||
return res
|
||||
}
|
||||
|
||||
export default escape
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useBoolean } from 'ahooks'
|
||||
@ -13,6 +13,8 @@ import { groupBy } from 'lodash-es'
|
||||
import PreviewItem, { PreviewType } from './preview-item'
|
||||
import LanguageSelect from './language-select'
|
||||
import s from './index.module.css'
|
||||
import unescape from './unescape'
|
||||
import escape from './escape'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { CrawlOptions, CrawlResultItem, CreateDocumentReq, CustomFile, FileIndexingEstimateResponse, FullDocumentDetail, IndexingEstimateParams, NotionInfo, PreProcessingRule, ProcessRule, Rules, createDocumentResponse } from '@/models/datasets'
|
||||
import {
|
||||
@ -78,6 +80,8 @@ enum IndexingType {
|
||||
ECONOMICAL = 'economy',
|
||||
}
|
||||
|
||||
const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
|
||||
|
||||
const StepTwo = ({
|
||||
isSetting,
|
||||
documentDetail,
|
||||
@ -110,8 +114,11 @@ const StepTwo = ({
|
||||
const previewScrollRef = useRef<HTMLDivElement>(null)
|
||||
const [previewScrolled, setPreviewScrolled] = useState(false)
|
||||
const [segmentationType, setSegmentationType] = useState<SegmentType>(SegmentType.AUTO)
|
||||
const [segmentIdentifier, setSegmentIdentifier] = useState('\\n')
|
||||
const [max, setMax] = useState(5000) // default chunk length
|
||||
const [segmentIdentifier, doSetSegmentIdentifier] = useState(DEFAULT_SEGMENT_IDENTIFIER)
|
||||
const setSegmentIdentifier = useCallback((value: string) => {
|
||||
doSetSegmentIdentifier(value ? escape(value) : DEFAULT_SEGMENT_IDENTIFIER)
|
||||
}, [])
|
||||
const [max, setMax] = useState(4000) // default chunk length
|
||||
const [overlap, setOverlap] = useState(50)
|
||||
const [rules, setRules] = useState<PreProcessingRule[]>([])
|
||||
const [defaultConfig, setDefaultConfig] = useState<Rules>()
|
||||
@ -183,7 +190,7 @@ const StepTwo = ({
|
||||
}
|
||||
const resetRules = () => {
|
||||
if (defaultConfig) {
|
||||
setSegmentIdentifier((defaultConfig.segmentation.separator === '\n' ? '\\n' : defaultConfig.segmentation.separator) || '\\n')
|
||||
setSegmentIdentifier(defaultConfig.segmentation.separator)
|
||||
setMax(defaultConfig.segmentation.max_tokens)
|
||||
setOverlap(defaultConfig.segmentation.chunk_overlap)
|
||||
setRules(defaultConfig.pre_processing_rules)
|
||||
@ -217,7 +224,7 @@ const StepTwo = ({
|
||||
const ruleObj = {
|
||||
pre_processing_rules: rules,
|
||||
segmentation: {
|
||||
separator: segmentIdentifier === '\\n' ? '\n' : segmentIdentifier,
|
||||
separator: unescape(segmentIdentifier),
|
||||
max_tokens: max,
|
||||
chunk_overlap: overlap,
|
||||
},
|
||||
@ -394,7 +401,7 @@ const StepTwo = ({
|
||||
try {
|
||||
const res = await fetchDefaultProcessRule({ url: '/datasets/process-rule' })
|
||||
const separator = res.rules.segmentation.separator
|
||||
setSegmentIdentifier((separator === '\n' ? '\\n' : separator) || '\\n')
|
||||
setSegmentIdentifier(separator)
|
||||
setMax(res.rules.segmentation.max_tokens)
|
||||
setOverlap(res.rules.segmentation.chunk_overlap)
|
||||
setRules(res.rules.pre_processing_rules)
|
||||
@ -411,7 +418,7 @@ const StepTwo = ({
|
||||
const separator = rules.segmentation.separator
|
||||
const max = rules.segmentation.max_tokens
|
||||
const overlap = rules.segmentation.chunk_overlap
|
||||
setSegmentIdentifier((separator === '\n' ? '\\n' : separator) || '\\n')
|
||||
setSegmentIdentifier(separator)
|
||||
setMax(max)
|
||||
setOverlap(overlap)
|
||||
setRules(rules.pre_processing_rules)
|
||||
@ -616,12 +623,22 @@ const StepTwo = ({
|
||||
<div className={s.typeFormBody}>
|
||||
<div className={s.formRow}>
|
||||
<div className='w-full'>
|
||||
<div className={s.label}>{t('datasetCreation.stepTwo.separator')}</div>
|
||||
<div className={s.label}>
|
||||
{t('datasetCreation.stepTwo.separator')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[200px]'>
|
||||
{t('datasetCreation.stepTwo.separatorTip')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className={s.input}
|
||||
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder') || ''} value={segmentIdentifier}
|
||||
onChange={e => setSegmentIdentifier(e.target.value)}
|
||||
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder') || ''}
|
||||
value={segmentIdentifier}
|
||||
onChange={e => doSetSegmentIdentifier(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -803,7 +820,7 @@ const StepTwo = ({
|
||||
<div className={s.label}>
|
||||
<div className='shrink-0 mr-4'>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.longDescription')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
54
web/app/components/datasets/create/step-two/unescape.ts
Normal file
54
web/app/components/datasets/create/step-two/unescape.ts
Normal file
@ -0,0 +1,54 @@
|
||||
// https://github.com/iamakulov/unescape-js/blob/master/src/index.js
|
||||
|
||||
/**
|
||||
* \\ - matches the backslash which indicates the beginning of an escape sequence
|
||||
* (
|
||||
* u\{([0-9A-Fa-f]+)\} - first alternative; matches the variable-length hexadecimal escape sequence (\u{ABCD0})
|
||||
* |
|
||||
* u([0-9A-Fa-f]{4}) - second alternative; matches the 4-digit hexadecimal escape sequence (\uABCD)
|
||||
* |
|
||||
* x([0-9A-Fa-f]{2}) - third alternative; matches the 2-digit hexadecimal escape sequence (\xA5)
|
||||
* |
|
||||
* ([1-7][0-7]{0,2}|[0-7]{2,3}) - fourth alternative; matches the up-to-3-digit octal escape sequence (\5 or \512)
|
||||
* |
|
||||
* (['"tbrnfv0\\]) - fifth alternative; matches the special escape characters (\t, \n and so on)
|
||||
* |
|
||||
* \U([0-9A-Fa-f]+) - sixth alternative; matches the 8-digit hexadecimal escape sequence used by python (\U0001F3B5)
|
||||
* )
|
||||
*/
|
||||
const jsEscapeRegex = /\\(u\{([0-9A-Fa-f]+)\}|u([0-9A-Fa-f]{4})|x([0-9A-Fa-f]{2})|([1-7][0-7]{0,2}|[0-7]{2,3})|(['"tbrnfv0\\]))|\\U([0-9A-Fa-f]{8})/g
|
||||
|
||||
const usualEscapeSequences: Record<string, string> = {
|
||||
'0': '\0',
|
||||
'b': '\b',
|
||||
'f': '\f',
|
||||
'n': '\n',
|
||||
'r': '\r',
|
||||
't': '\t',
|
||||
'v': '\v',
|
||||
'\'': '\'',
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
}
|
||||
|
||||
const fromHex = (str: string) => String.fromCodePoint(parseInt(str, 16))
|
||||
const fromOct = (str: string) => String.fromCodePoint(parseInt(str, 8))
|
||||
|
||||
const unescape = (str: string) => {
|
||||
return str.replace(jsEscapeRegex, (_, __, varHex, longHex, shortHex, octal, specialCharacter, python) => {
|
||||
if (varHex !== undefined)
|
||||
return fromHex(varHex)
|
||||
else if (longHex !== undefined)
|
||||
return fromHex(longHex)
|
||||
else if (shortHex !== undefined)
|
||||
return fromHex(shortHex)
|
||||
else if (octal !== undefined)
|
||||
return fromOct(octal)
|
||||
else if (python !== undefined)
|
||||
return fromHex(python)
|
||||
else
|
||||
return usualEscapeSequences[specialCharacter]
|
||||
})
|
||||
}
|
||||
|
||||
export default unescape
|
||||
@ -77,7 +77,7 @@ const ModifyRetrievalModal: FC<Props> = ({
|
||||
<div className='text-base font-semibold text-gray-900'>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -245,7 +245,7 @@ const Form = () => {
|
||||
<div>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-6-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,6 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
@ -17,50 +16,70 @@ import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import { fetchConversationMessages } from '@/service/debug'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { UUID_NIL } from '@/app/components/base/chat/constants'
|
||||
|
||||
function appendQAToChatList(newChatList: ChatItem[], item: any) {
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.metadata?.retriever_resources,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
})
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
}
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
let nextMessageId = null
|
||||
for (const item of messages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(newChatList, item)
|
||||
break
|
||||
}
|
||||
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(newChatList, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(newChatList, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return newChatList.reverse()
|
||||
}
|
||||
|
||||
const ChatRecord = () => {
|
||||
const [fetched, setFetched] = useState(false)
|
||||
const [chatList, setChatList] = useState([])
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleLoadBackupDraft } = useWorkflowRun()
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const currentConversationID = historyWorkflowData?.conversation_id
|
||||
|
||||
const chatMessageList = useMemo(() => {
|
||||
const res: ChatItem[] = []
|
||||
if (chatList.length) {
|
||||
chatList.forEach((item: any) => {
|
||||
res.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
|
||||
})
|
||||
res.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.metadata?.retriever_resources,
|
||||
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
|
||||
workflow_run_id: item.workflow_run_id,
|
||||
})
|
||||
})
|
||||
}
|
||||
return res
|
||||
}, [chatList])
|
||||
|
||||
const handleFetchConversationMessages = useCallback(async () => {
|
||||
if (appDetail && currentConversationID) {
|
||||
try {
|
||||
setFetched(false)
|
||||
const res = await fetchConversationMessages(appDetail.id, currentConversationID)
|
||||
setFetched(true)
|
||||
setChatList((res as any).data)
|
||||
setChatList(getFormattedChatList((res as any).data))
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
console.error(e)
|
||||
}
|
||||
finally {
|
||||
setFetched(true)
|
||||
}
|
||||
}
|
||||
}, [appDetail, currentConversationID])
|
||||
@ -101,7 +120,7 @@ const ChatRecord = () => {
|
||||
config={{
|
||||
supportCitationHitInfo: true,
|
||||
} as any}
|
||||
chatList={chatMessageList}
|
||||
chatList={chatList}
|
||||
chatContainerClassName='px-4'
|
||||
chatContainerInnerClassName='pt-6 w-full max-w-full mx-auto'
|
||||
chatFooterClassName='px-4 rounded-b-2xl'
|
||||
|
||||
@ -18,7 +18,7 @@ import ConversationVariableModal from './conversation-variable-modal'
|
||||
import { useChat } from './hooks'
|
||||
import type { ChatWrapperRefType } from './index'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import type { OnSend } from '@/app/components/base/chat/types'
|
||||
import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import {
|
||||
fetchSuggestedQuestions,
|
||||
@ -58,6 +58,8 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
||||
const {
|
||||
conversationId,
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
handleStop,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
@ -73,19 +75,42 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
||||
taskId => stopChatMessageResponding(appDetail!.id, taskId),
|
||||
)
|
||||
|
||||
const doSend = useCallback<OnSend>((query, files) => {
|
||||
const doSend = useCallback<OnSend>((query, files, last_answer) => {
|
||||
const lastAnswer = chatListRef.current.at(-1)
|
||||
|
||||
handleSend(
|
||||
{
|
||||
query,
|
||||
files,
|
||||
inputs: workflowStore.getState().inputs,
|
||||
conversation_id: conversationId,
|
||||
parent_message_id: last_answer?.id || (lastAnswer
|
||||
? lastAnswer.isOpeningStatement
|
||||
? null
|
||||
: lastAnswer.id
|
||||
: null),
|
||||
},
|
||||
{
|
||||
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
|
||||
},
|
||||
)
|
||||
}, [conversationId, handleSend, workflowStore, appDetail])
|
||||
}, [chatListRef, conversationId, handleSend, workflowStore, appDetail])
|
||||
|
||||
const doRegenerate = useCallback((chatItem: ChatItem) => {
|
||||
const index = chatList.findIndex(item => item.id === chatItem.id)
|
||||
if (index === -1)
|
||||
return
|
||||
|
||||
const prevMessages = chatList.slice(0, index)
|
||||
const question = prevMessages.pop()
|
||||
const lastAnswer = prevMessages.at(-1)
|
||||
|
||||
if (!question)
|
||||
return
|
||||
|
||||
handleUpdateChatList(prevMessages)
|
||||
doSend(question.content, question.message_files, (!lastAnswer || lastAnswer.isOpeningStatement) ? undefined : lastAnswer)
|
||||
}, [chatList, handleUpdateChatList, doSend])
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
@ -107,6 +132,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType, ChatWrapperProps>(({ showConv
|
||||
chatFooterClassName='px-4 rounded-bl-2xl'
|
||||
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
|
||||
onSend={doSend}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
chatNode={(
|
||||
<>
|
||||
|
||||
@ -387,6 +387,8 @@ export const useChat = (
|
||||
return {
|
||||
conversationId: conversationId.current,
|
||||
chatList,
|
||||
chatListRef,
|
||||
handleUpdateChatList,
|
||||
handleSend,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
|
||||
@ -217,6 +217,7 @@ const NormalForm = () => {
|
||||
autoComplete="email"
|
||||
placeholder={t('login.emailPlaceholder') || ''}
|
||||
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -241,6 +242,7 @@ const NormalForm = () => {
|
||||
autoComplete="current-password"
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
|
||||
tabIndex={2}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<button
|
||||
|
||||
@ -5,10 +5,10 @@ import type { AppIconType } from '@/types/app'
|
||||
|
||||
type UseAppFaviconOptions = {
|
||||
enable?: boolean
|
||||
icon_type?: AppIconType
|
||||
icon_type?: AppIconType | null
|
||||
icon?: string
|
||||
icon_background?: string
|
||||
icon_url?: string
|
||||
icon_background?: string | null
|
||||
icon_url?: string | null
|
||||
}
|
||||
|
||||
export function useAppFavicon(options: UseAppFaviconOptions) {
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'Wissenseinstellungen.',
|
||||
websiteSource: 'Preprocess-Website',
|
||||
webpageUnit: 'Seiten',
|
||||
separatorTip: 'Ein Trennzeichen ist das Zeichen, das zum Trennen von Text verwendet wird. \\n\\n und \\n sind häufig verwendete Trennzeichen zum Trennen von Absätzen und Zeilen. In Kombination mit Kommas (\\n\\n,\\n) werden Absätze nach Zeilen segmentiert, wenn die maximale Blocklänge überschritten wird. Sie können auch spezielle, von Ihnen selbst definierte Trennzeichen verwenden (z. B. ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Wissen erstellt',
|
||||
|
||||
@ -6,6 +6,7 @@ const translation = {
|
||||
ok: 'In Service',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
regenerate: 'Regenerate',
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
playing: 'Playing',
|
||||
|
||||
@ -87,7 +87,8 @@ const translation = {
|
||||
custom: 'Custom',
|
||||
customDescription: 'Customize chunks rules, chunks length, and preprocessing rules, etc.',
|
||||
separator: 'Delimiter',
|
||||
separatorPlaceholder: 'For example, newline (\\\\n) or special separator (such as "***")',
|
||||
separatorTip: 'A delimiter is the character used to separate text. \\n\\n and \\n are commonly used delimiters for separating paragraphs and lines. Combined with commas (\\n\\n,\\n), paragraphs will be segmented by lines when exceeding the maximum chunk length. You can also use special delimiters defined by yourself (e.g. ***).',
|
||||
separatorPlaceholder: '\\n\\n for separating paragraphs; \\n for separating lines',
|
||||
maxLength: 'Maximum chunk length',
|
||||
overlap: 'Chunk overlap',
|
||||
overlapTip: 'Setting the chunk overlap can maintain the semantic relevance between them, enhancing the retrieve effect. It is recommended to set 10%-25% of the maximum chunk size.',
|
||||
|
||||
@ -138,6 +138,7 @@ const translation = {
|
||||
indexSettingTip: 'Para cambiar el método de índice, por favor ve a la ',
|
||||
retrievalSettingTip: 'Para cambiar el método de índice, por favor ve a la ',
|
||||
datasetSettingLink: 'configuración del conocimiento.',
|
||||
separatorTip: 'Un delimitador es el carácter que se utiliza para separar el texto. \\n\\n y \\n son delimitadores comúnmente utilizados para separar párrafos y líneas. Combinado con comas (\\n\\n,\\n), los párrafos se segmentarán por líneas cuando excedan la longitud máxima del fragmento. También puede utilizar delimitadores especiales definidos por usted mismo (por ejemplo, ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Conocimiento creado',
|
||||
|
||||
@ -138,6 +138,7 @@ const translation = {
|
||||
indexSettingTip: 'برای تغییر روش شاخص، لطفاً به',
|
||||
retrievalSettingTip: 'برای تغییر روش شاخص، لطفاً به',
|
||||
datasetSettingLink: 'تنظیمات دانش بروید.',
|
||||
separatorTip: 'جداکننده نویسه ای است که برای جداسازی متن استفاده می شود. \\n\\n و \\n معمولا برای جداسازی پاراگراف ها و خطوط استفاده می شوند. همراه با کاما (\\n\\n,\\n)، پاراگراف ها زمانی که از حداکثر طول تکه فراتر می روند، با خطوط تقسیم بندی می شوند. همچنین می توانید از جداکننده های خاصی که توسط خودتان تعریف شده اند استفاده کنید (مثلا ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: ' دانش ایجاد شد',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'Paramètres de connaissance.',
|
||||
webpageUnit: 'Pages',
|
||||
websiteSource: 'Site web de prétraitement',
|
||||
separatorTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n et \\n sont des délimiteurs couramment utilisés pour séparer les paragraphes et les lignes. Combiné à des virgules (\\n\\n,\\n), les paragraphes seront segmentés par des lignes lorsqu’ils dépasseront la longueur maximale des morceaux. Vous pouvez également utiliser des délimiteurs spéciaux définis par vous-même (par exemple ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Connaissance créée',
|
||||
|
||||
@ -155,6 +155,7 @@ const translation = {
|
||||
indexSettingTip: 'इंडेक्स विधि बदलने के लिए, कृपया जाएं ',
|
||||
retrievalSettingTip: 'इंडेक्स विधि बदलने के लिए, कृपया जाएं ',
|
||||
datasetSettingLink: 'ज्ञान सेटिंग्स।',
|
||||
separatorTip: 'एक सीमांकक पाठ को अलग करने के लिए उपयोग किया जाने वाला वर्ण है। \\n\\n और \\n आमतौर पर पैराग्राफ और लाइनों को अलग करने के लिए उपयोग किए जाने वाले सीमांकक हैं। अल्पविराम (\\n\\n,\\n) के साथ संयुक्त, अधिकतम खंड लंबाई से अधिक होने पर अनुच्छेदों को पंक्तियों द्वारा खंडित किया जाएगा। आप स्वयं द्वारा परिभाषित विशेष सीमांकक का भी उपयोग कर सकते हैं (उदा. ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 ज्ञान बनाया गया',
|
||||
|
||||
@ -158,6 +158,7 @@ const translation = {
|
||||
indexSettingTip: 'Per cambiare il metodo di indicizzazione, vai alle ',
|
||||
retrievalSettingTip: 'Per cambiare il metodo di indicizzazione, vai alle ',
|
||||
datasetSettingLink: 'impostazioni della Conoscenza.',
|
||||
separatorTip: 'Un delimitatore è il carattere utilizzato per separare il testo. \\n\\n e \\n sono delimitatori comunemente usati per separare paragrafi e righe. In combinazione con le virgole (\\n\\n,\\n), i paragrafi verranno segmentati per righe quando superano la lunghezza massima del blocco. È inoltre possibile utilizzare delimitatori speciali definiti dall\'utente (ad es. ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Conoscenza creata',
|
||||
|
||||
@ -138,6 +138,7 @@ const translation = {
|
||||
indexSettingTip: 'インデックス方法を変更するには、',
|
||||
retrievalSettingTip: '検索方法を変更するには、',
|
||||
datasetSettingLink: 'ナレッジ設定',
|
||||
separatorTip: '区切り文字は、テキストを区切るために使用される文字です。\\n\\n と \\n は、段落と行を区切るために一般的に使用される区切り記号です。カンマ (\\n\\n,\\n) と組み合わせると、最大チャンク長を超えると、段落は行で区切られます。自分で定義した特別な区切り文字を使用することもできます(例:***)。',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 ナレッジが作成されました',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: '지식 설정',
|
||||
webpageUnit: '페이지',
|
||||
websiteSource: '웹 사이트 전처리',
|
||||
separatorTip: '구분 기호는 텍스트를 구분하는 데 사용되는 문자입니다. \\n\\n 및 \\n은 단락과 줄을 구분하는 데 일반적으로 사용되는 구분 기호입니다. 쉼표(\\n\\n,\\n)와 함께 사용하면 최대 청크 길이를 초과할 경우 단락이 줄로 분할됩니다. 직접 정의한 특수 구분 기호(예: ***)를 사용할 수도 있습니다.',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 지식이 생성되었습니다',
|
||||
|
||||
@ -146,6 +146,7 @@ const translation = {
|
||||
datasetSettingLink: 'ustawień Wiedzy.',
|
||||
webpageUnit: 'Stron',
|
||||
websiteSource: 'Witryna internetowa przetwarzania wstępnego',
|
||||
separatorTip: 'Ogranicznik to znak używany do oddzielania tekstu. \\n\\n i \\n są powszechnie używanymi ogranicznikami do oddzielania akapitów i wierszy. W połączeniu z przecinkami (\\n\\n,\\n), akapity będą segmentowane wierszami po przekroczeniu maksymalnej długości fragmentu. Możesz również skorzystać ze zdefiniowanych przez siebie specjalnych ograniczników (np. ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Utworzono Wiedzę',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'configurações do Conhecimento.',
|
||||
websiteSource: 'Site de pré-processamento',
|
||||
webpageUnit: 'Páginas',
|
||||
separatorTip: 'Um delimitador é o caractere usado para separar o texto. \\n\\n e \\n são delimitadores comumente usados para separar parágrafos e linhas. Combinado com vírgulas (\\n\\n,\\n), os parágrafos serão segmentados por linhas ao exceder o comprimento máximo do bloco. Você também pode usar delimitadores especiais definidos por você (por exemplo, ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Conhecimento criado',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'setările Cunoștinței.',
|
||||
webpageUnit: 'Pagini',
|
||||
websiteSource: 'Site-ul web de preprocesare',
|
||||
separatorTip: 'Un delimitator este caracterul folosit pentru a separa textul. \\n\\n și \\n sunt delimitatori utilizați în mod obișnuit pentru separarea paragrafelor și liniilor. Combinate cu virgule (\\n\\n,\\n), paragrafele vor fi segmentate pe linii atunci când depășesc lungimea maximă a bucății. De asemenea, puteți utiliza delimitatori speciali definiți de dumneavoastră (de exemplu, ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Cunoștință creată',
|
||||
|
||||
@ -138,6 +138,7 @@ const translation = {
|
||||
indexSettingTip: 'Чтобы изменить метод индексации, пожалуйста, перейдите в ',
|
||||
retrievalSettingTip: 'Чтобы изменить метод индексации, пожалуйста, перейдите в ',
|
||||
datasetSettingLink: 'настройки базы знаний.',
|
||||
separatorTip: 'Разделитель — это символ, используемый для разделения текста. \\n\\n и \\n — это часто используемые разделители для разделения абзацев и строк. В сочетании с запятыми (\\n\\n,\\n) абзацы будут сегментированы по строкам, если максимальная длина блока превышает их. Вы также можете использовать специальные разделители, определенные вами (например, ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 База знаний создана',
|
||||
|
||||
@ -138,6 +138,7 @@ const translation = {
|
||||
indexSettingTip: 'Dizin yöntemini değiştirmek için, lütfen',
|
||||
retrievalSettingTip: 'Dizin yöntemini değiştirmek için, lütfen',
|
||||
datasetSettingLink: 'Bilgi ayarlarına gidin.',
|
||||
separatorTip: 'Sınırlayıcı, metni ayırmak için kullanılan karakterdir. \\n\\n ve \\n, paragrafları ve satırları ayırmak için yaygın olarak kullanılan sınırlayıcılardır. Virgüllerle (\\n\\n,\\n) birleştirildiğinde, paragraflar maksimum öbek uzunluğunu aştığında satırlarla bölünür. Kendiniz tarafından tanımlanan özel sınırlayıcıları da kullanabilirsiniz (örn.',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Bilgi oluşturuldu',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'Налаштування знань.',
|
||||
webpageUnit: 'Сторінок',
|
||||
websiteSource: 'Веб-сайт попередньої обробки',
|
||||
separatorTip: 'Роздільник – це символ, який використовується для поділу тексту. \\n\\n та \\n є часто використовуваними роздільниками для відокремлення абзаців та рядків. У поєднанні з комами (\\n\\n,\\n) абзаци будуть розділені лініями, якщо вони перевищують максимальну довжину фрагмента. Ви також можете використовувати спеціальні роздільники, визначені вами (наприклад, ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Знання створено',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: 'cài đặt Kiến thức.',
|
||||
websiteSource: 'Trang web tiền xử lý',
|
||||
webpageUnit: 'Trang',
|
||||
separatorTip: 'Dấu phân cách là ký tự được sử dụng để phân tách văn bản. \\n\\n và \\n là dấu phân cách thường được sử dụng để tách các đoạn văn và dòng. Kết hợp với dấu phẩy (\\n\\n,\\n), các đoạn văn sẽ được phân đoạn theo các dòng khi vượt quá độ dài đoạn tối đa. Bạn cũng có thể sử dụng dấu phân cách đặc biệt do chính bạn xác định (ví dụ: ***).',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 Kiến thức đã được tạo',
|
||||
|
||||
@ -6,6 +6,7 @@ const translation = {
|
||||
ok: '运行中',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
regenerate: '重新生成',
|
||||
play: '播放',
|
||||
pause: '暂停',
|
||||
playing: '播放中',
|
||||
|
||||
@ -87,7 +87,8 @@ const translation = {
|
||||
custom: '自定义',
|
||||
customDescription: '自定义分段规则、分段长度以及预处理规则等参数',
|
||||
separator: '分段标识符',
|
||||
separatorPlaceholder: '例如换行符(\n)或特定的分隔符(如 "***")',
|
||||
separatorTip: '分隔符是用于分隔文本的字符。\\n\\n 和 \\n 是常用于分隔段落和行的分隔符。用逗号连接分隔符(\\n\\n,\\n),当段落超过最大块长度时,会按行进行分割。你也可以使用自定义的特殊分隔符(例如 ***)。',
|
||||
separatorPlaceholder: '\\n\\n 用于分段;\\n 用于分行',
|
||||
maxLength: '分段最大长度',
|
||||
overlap: '分段重叠长度',
|
||||
overlapTip: '设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果。建议设置为最大分段长度的10%-25%',
|
||||
|
||||
@ -133,6 +133,7 @@ const translation = {
|
||||
datasetSettingLink: '知識庫設定。',
|
||||
websiteSource: '預處理網站',
|
||||
webpageUnit: '頁面',
|
||||
separatorTip: '分隔符是用於分隔文字的字元。\\n\\n 和 \\n 是分隔段落和行的常用分隔符。與逗號 (\\n\\n,\\n) 組合使用時,當超過最大區塊長度時,段落將按行分段。您也可以使用自定義的特殊分隔符(例如 ***)。',
|
||||
},
|
||||
stepThree: {
|
||||
creationTitle: '🎉 知識庫已建立',
|
||||
|
||||
@ -106,6 +106,7 @@ export type MessageContent = {
|
||||
metadata: Metadata
|
||||
agent_thoughts: any[] // TODO
|
||||
workflow_run_id: string
|
||||
parent_message_id: string | null
|
||||
}
|
||||
|
||||
export type CompletionConversationGeneralDetail = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "0.8.2",
|
||||
"version": "0.8.3",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
@ -37,6 +37,7 @@
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@sentry/react": "^7.54.0",
|
||||
"@sentry/utils": "^7.54.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"ahooks": "^3.7.5",
|
||||
@ -44,7 +45,6 @@
|
||||
"classnames": "^2.3.2",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
|
||||
61
web/utils/format.spec.ts
Normal file
61
web/utils/format.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { formatFileSize, formatNumber, formatTime } from './format'
|
||||
describe('formatNumber', () => {
|
||||
test('should correctly format integers', () => {
|
||||
expect(formatNumber(1234567)).toBe('1,234,567')
|
||||
})
|
||||
test('should correctly format decimals', () => {
|
||||
expect(formatNumber(1234567.89)).toBe('1,234,567.89')
|
||||
})
|
||||
test('should correctly handle string input', () => {
|
||||
expect(formatNumber('1234567')).toBe('1,234,567')
|
||||
})
|
||||
test('should correctly handle zero', () => {
|
||||
expect(formatNumber(0)).toBe(0)
|
||||
})
|
||||
test('should correctly handle negative numbers', () => {
|
||||
expect(formatNumber(-1234567)).toBe('-1,234,567')
|
||||
})
|
||||
test('should correctly handle empty input', () => {
|
||||
expect(formatNumber('')).toBe('')
|
||||
})
|
||||
})
|
||||
describe('formatFileSize', () => {
|
||||
test('should return the input if it is falsy', () => {
|
||||
expect(formatFileSize(0)).toBe(0)
|
||||
})
|
||||
test('should format bytes correctly', () => {
|
||||
expect(formatFileSize(500)).toBe('500.00B')
|
||||
})
|
||||
test('should format kilobytes correctly', () => {
|
||||
expect(formatFileSize(1500)).toBe('1.46KB')
|
||||
})
|
||||
test('should format megabytes correctly', () => {
|
||||
expect(formatFileSize(1500000)).toBe('1.43MB')
|
||||
})
|
||||
test('should format gigabytes correctly', () => {
|
||||
expect(formatFileSize(1500000000)).toBe('1.40GB')
|
||||
})
|
||||
test('should format terabytes correctly', () => {
|
||||
expect(formatFileSize(1500000000000)).toBe('1.36TB')
|
||||
})
|
||||
test('should format petabytes correctly', () => {
|
||||
expect(formatFileSize(1500000000000000)).toBe('1.33PB')
|
||||
})
|
||||
})
|
||||
describe('formatTime', () => {
|
||||
test('should return the input if it is falsy', () => {
|
||||
expect(formatTime(0)).toBe(0)
|
||||
})
|
||||
test('should format seconds correctly', () => {
|
||||
expect(formatTime(30)).toBe('30.00 sec')
|
||||
})
|
||||
test('should format minutes correctly', () => {
|
||||
expect(formatTime(90)).toBe('1.50 min')
|
||||
})
|
||||
test('should format hours correctly', () => {
|
||||
expect(formatTime(3600)).toBe('1.00 h')
|
||||
})
|
||||
test('should handle large numbers', () => {
|
||||
expect(formatTime(7200)).toBe('2.00 h')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user