+
+
handleFeedback('like')}
diff --git a/web/app/components/base/chat/chat/context.tsx b/web/app/components/base/chat/chat/context.tsx
index ba6f67189e..c47b750176 100644
--- a/web/app/components/base/chat/chat/context.tsx
+++ b/web/app/components/base/chat/chat/context.tsx
@@ -12,6 +12,7 @@ export type ChatContextValue = Pick
{
const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) {
- const requestion = draft[index - 1]
+ const question = draft[index - 1]
draft[index - 1] = {
- ...requestion,
+ ...question,
}
draft[index] = {
...draft[index],
@@ -647,7 +647,8 @@ export const useChat = (
return {
chatList,
- setChatList,
+ chatListRef,
+ handleUpdateChatList,
conversationId: conversationId.current,
isResponding,
setIsResponding,
diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx
index 68194193c4..2847ae20c3 100644
--- a/web/app/components/base/chat/chat/index.tsx
+++ b/web/app/components/base/chat/chat/index.tsx
@@ -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 = ({
appData,
config,
onSend,
+ onRegenerate,
chatList,
isResponding,
noStopResponding,
@@ -186,6 +189,7 @@ const Chat: FC = ({
answerIcon={answerIcon}
allToolIcons={allToolIcons}
onSend={onSend}
+ onRegenerate={onRegenerate}
onAnnotationAdded={onAnnotationAdded}
onAnnotationEdited={onAnnotationEdited}
onAnnotationRemoved={onAnnotationRemoved}
@@ -219,6 +223,7 @@ const Chat: FC = ({
showPromptLog={showPromptLog}
chatAnswerContainerInner={chatAnswerContainerInner}
hideProcessDetail={hideProcessDetail}
+ noChatInput={noChatInput}
/>
)
}
diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts
index b2cb18011c..dd26a4179d 100644
--- a/web/app/components/base/chat/chat/type.ts
+++ b/web/app/components/base/chat/chat/type.ts
@@ -95,6 +95,7 @@ export type IChatItem = {
// for agent log
conversationId?: string
input?: any
+ parentMessageId?: string
}
export type Metadata = {
diff --git a/web/app/components/base/chat/constants.ts b/web/app/components/base/chat/constants.ts
index 8249be7375..309f0f04a7 100644
--- a/web/app/components/base/chat/constants.ts
+++ b/web/app/components/base/chat/constants.ts
@@ -1 +1,2 @@
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
+export const UUID_NIL = '00000000-0000-0000-0000-000000000000'
diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
index 48ee411058..b97c940eec 100644
--- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
@@ -2,9 +2,11 @@ import { useCallback, useEffect, useMemo } from 'react'
import Chat from '../chat'
import type {
ChatConfig,
+ ChatItem,
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
+import { getLastAnswer } from '../utils'
import { useEmbeddedChatbotContext } from './context'
import ConfigPanel from './config-panel'
import { isDify } from './utils'
@@ -45,11 +47,13 @@ const ChatWrapper = () => {
} as ChatConfig
}, [appParams, currentConversationItem?.introduction, currentConversationId])
const {
+ chatListRef,
chatList,
handleSend,
handleStop,
isResponding,
suggestedQuestions,
+ handleUpdateChatList,
} = useChat(
appConfig,
{
@@ -65,11 +69,12 @@ const ChatWrapper = () => {
currentChatInstanceRef.current.handleStop = handleStop
}, [])
- const doSend: OnSend = useCallback((message, files) => {
+ const doSend: OnSend = useCallback((message, files, last_answer) => {
const data: any = {
query: message,
inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
conversation_id: currentConversationId,
+ parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
}
if (appConfig?.file_upload?.image.enabled && files?.length)
@@ -85,6 +90,7 @@ const ChatWrapper = () => {
},
)
}, [
+ chatListRef,
appConfig,
currentConversationId,
currentConversationItem,
@@ -94,6 +100,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 = getLastAnswer(prevMessages)
+
+ if (!question)
+ return
+
+ handleUpdateChatList(prevMessages)
+ doSend(question.content, question.message_files, lastAnswer)
+ }, [chatList, handleUpdateChatList, doSend])
+
const chatNode = useMemo(() => {
if (inputsForms.length) {
return (
@@ -136,6 +159,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 || {}}
diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx
index 39d25f57d1..fd89efcbff 100644
--- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx
@@ -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
diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts
index 21277fec57..0bc50518eb 100644
--- a/web/app/components/base/chat/types.ts
+++ b/web/app/components/base/chat/types.ts
@@ -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 | null) => void
+
+export type OnRegenerate = (chatItem: ChatItem) => void
export type Callback = {
onSuccess: () => void
diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts
index 3fe5050cc7..305df5995d 100644
--- a/web/app/components/base/chat/utils.ts
+++ b/web/app/components/base/chat/utils.ts
@@ -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,67 @@ function getProcessedInputsFromUrlParams(): Record {
return inputs
}
+function getLastAnswer(chatList: ChatItem[]) {
+ for (let i = chatList.length - 1; i >= 0; i--) {
+ const item = chatList[i]
+ if (item.isAnswer && !item.isOpeningStatement)
+ return item
+ }
+ return null
+}
+
+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,
+ getLastAnswer,
+ getPrevChatList,
}
diff --git a/web/app/components/base/icons/assets/vender/line/general/refresh.svg b/web/app/components/base/icons/assets/vender/line/general/refresh.svg
new file mode 100644
index 0000000000..05cf986827
--- /dev/null
+++ b/web/app/components/base/icons/assets/vender/line/general/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/vender/line/general/Refresh.json b/web/app/components/base/icons/src/vender/line/general/Refresh.json
new file mode 100644
index 0000000000..128dcb7d4d
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/Refresh.json
@@ -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"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/vender/line/general/Refresh.tsx b/web/app/components/base/icons/src/vender/line/general/Refresh.tsx
new file mode 100644
index 0000000000..96641f1c42
--- /dev/null
+++ b/web/app/components/base/icons/src/vender/line/general/Refresh.tsx
@@ -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, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'Refresh'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts
index c1af2e4994..b5c7a7bbc1 100644
--- a/web/app/components/base/icons/src/vender/line/general/index.ts
+++ b/web/app/components/base/icons/src/vender/line/general/index.ts
@@ -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'
diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx
index e5bd4c1bbc..096facabfd 100644
--- a/web/app/components/base/image-uploader/image-preview.tsx
+++ b/web/app/components/base/image-uploader/image-preview.tsx
@@ -88,7 +88,7 @@ const ImagePreview: FC = ({
})
}
- const imageTobase64ToBlob = (base64: string, type = 'image/png'): Blob => {
+ const imageBase64ToBlob = (base64: string, type = 'image/png'): Blob => {
const byteCharacters = atob(base64)
const byteArrays = []
@@ -109,7 +109,7 @@ const ImagePreview: FC = ({
const shareImage = async () => {
try {
const base64Data = url.split(',')[1]
- const blob = imageTobase64ToBlob(base64Data, 'image/png')
+ const blob = imageBase64ToBlob(base64Data, 'image/png')
await navigator.clipboard.write([
new ClipboardItem({
diff --git a/web/app/components/base/regenerate-btn/index.tsx b/web/app/components/base/regenerate-btn/index.tsx
new file mode 100644
index 0000000000..aaf0206df6
--- /dev/null
+++ b/web/app/components/base/regenerate-btn/index.tsx
@@ -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 (
+
+
+ onClick?.()}
+ style={{
+ boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
+ }}
+ >
+
+
+
+
+ )
+}
+
+export default RegenerateBtn
diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx
index 94614918db..f4fc58ee2a 100644
--- a/web/app/components/datasets/create/step-two/index.tsx
+++ b/web/app/components/datasets/create/step-two/index.tsx
@@ -820,7 +820,7 @@ const StepTwo = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
index 999f1cdf0d..1fc5b68d67 100644
--- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
+++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx
@@ -77,7 +77,7 @@ const ModifyRetrievalModal: FC = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx
index 0f6bdd0a59..15b8abc242 100644
--- a/web/app/components/datasets/settings/form/index.tsx
+++ b/web/app/components/datasets/settings/form/index.tsx
@@ -245,7 +245,7 @@ const Form = () => {
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx
index 2bd0fe9daf..5c712c2c29 100644
--- a/web/app/components/develop/template/template_workflow.en.mdx
+++ b/web/app/components/develop/template/template_workflow.en.mdx
@@ -424,7 +424,7 @@ Workflow applications offers non-session support and is ideal for translation, a
/>
- Returns worklfow logs, with the first page returning the latest `{limit}` messages, i.e., in reverse order.
+ Returns workflow logs, with the first page returning the latest `{limit}` messages, i.e., in reverse order.
### Query
diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts
index e1da503f38..68c3ff0a4b 100644
--- a/web/app/components/workflow/hooks/use-workflow-run.ts
+++ b/web/app/components/workflow/hooks/use-workflow-run.ts
@@ -185,7 +185,7 @@ export const useWorkflowRun = () => {
draft.forEach((edge) => {
edge.data = {
...edge.data,
- _runned: false,
+ _run: false,
}
})
})
@@ -292,7 +292,7 @@ export const useWorkflowRun = () => {
const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
if (edge.target === data.node_id && incomeNodesId.includes(edge.source))
- edge.data = { ...edge.data, _runned: true } as any
+ edge.data = { ...edge.data, _run: true } as any
})
})
setEdges(newEdges)
@@ -416,7 +416,7 @@ export const useWorkflowRun = () => {
const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId)
if (edge)
- edge.data = { ...edge.data, _runned: true } as any
+ edge.data = { ...edge.data, _run: true } as any
})
setEdges(newEdges)
diff --git a/web/app/components/workflow/panel/chat-record/index.tsx b/web/app/components/workflow/panel/chat-record/index.tsx
index afd20b7358..1bcfd6474d 100644
--- a/web/app/components/workflow/panel/chat-record/index.tsx
+++ b/web/app/components/workflow/panel/chat-record/index.tsx
@@ -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([])
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'
diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
index a7dd607e22..230b2d7fa0 100644
--- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
@@ -18,13 +18,14 @@ 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,
stopChatMessageResponding,
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
+import { getLastAnswer } from '@/app/components/base/chat/utils'
type ChatWrapperProps = {
showConversationVariableModal: boolean
@@ -58,6 +59,8 @@ const ChatWrapper = forwardRef(({ showConv
const {
conversationId,
chatList,
+ chatListRef,
+ handleUpdateChatList,
handleStop,
isResponding,
suggestedQuestions,
@@ -73,19 +76,36 @@ const ChatWrapper = forwardRef(({ showConv
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
- const doSend = useCallback((query, files) => {
+ const doSend = useCallback((query, files, last_answer) => {
handleSend(
{
query,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
+ parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.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 = getLastAnswer(prevMessages)
+
+ if (!question)
+ return
+
+ handleUpdateChatList(prevMessages)
+ doSend(question.content, question.message_files, lastAnswer)
+ }, [chatList, handleUpdateChatList, doSend])
useImperativeHandle(ref, () => {
return {
@@ -107,6 +127,7 @@ const ChatWrapper = forwardRef(({ showConv
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4 w-full max-w-full mx-auto'
onSend={doSend}
+ onRegenerate={doRegenerate}
onStopResponding={handleStop}
chatNode={(
<>
diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts
index 51a018bcb1..cad76a4490 100644
--- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts
+++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts
@@ -387,6 +387,8 @@ export const useChat = (
return {
conversationId: conversationId.current,
chatList,
+ chatListRef,
+ handleUpdateChatList,
handleSend,
handleStop,
handleRestart,
diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx
index 7f23c7d22e..816df8007d 100644
--- a/web/app/signin/normalForm.tsx
+++ b/web/app/signin/normalForm.tsx
@@ -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}
/>
@@ -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}
/>
item.supported).map(item => item.value)
export const getLanguage = (locale: string) => {
- if (locale === 'zh-Hans')
- return locale.replace('-', '_')
-
- return LanguagesSupported[0].replace('-', '_')
+ const supportedLocale = LanguagesSupported.find(lang => lang.startsWith(locale.split('-')[0]))
+ return (supportedLocale || LanguagesSupported[0]).replace('-', '_')
}
export const NOTICE_I18N = {
diff --git a/web/i18n/pl-PL/dataset-creation.ts b/web/i18n/pl-PL/dataset-creation.ts
index 64e50c6b33..f5b36e62ee 100644
--- a/web/i18n/pl-PL/dataset-creation.ts
+++ b/web/i18n/pl-PL/dataset-creation.ts
@@ -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ę',
diff --git a/web/i18n/pt-BR/dataset-creation.ts b/web/i18n/pt-BR/dataset-creation.ts
index 4ab78a50c7..511f0d5bcb 100644
--- a/web/i18n/pt-BR/dataset-creation.ts
+++ b/web/i18n/pt-BR/dataset-creation.ts
@@ -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',
diff --git a/web/i18n/ro-RO/dataset-creation.ts b/web/i18n/ro-RO/dataset-creation.ts
index efe3bb246c..4ea0b04758 100644
--- a/web/i18n/ro-RO/dataset-creation.ts
+++ b/web/i18n/ro-RO/dataset-creation.ts
@@ -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ă',
diff --git a/web/i18n/ru-RU/dataset-creation.ts b/web/i18n/ru-RU/dataset-creation.ts
index c4dce774d8..c97daeeece 100644
--- a/web/i18n/ru-RU/dataset-creation.ts
+++ b/web/i18n/ru-RU/dataset-creation.ts
@@ -138,6 +138,7 @@ const translation = {
indexSettingTip: 'Чтобы изменить метод индексации, пожалуйста, перейдите в ',
retrievalSettingTip: 'Чтобы изменить метод индексации, пожалуйста, перейдите в ',
datasetSettingLink: 'настройки базы знаний.',
+ separatorTip: 'Разделитель — это символ, используемый для разделения текста. \\n\\n и \\n — это часто используемые разделители для разделения абзацев и строк. В сочетании с запятыми (\\n\\n,\\n) абзацы будут сегментированы по строкам, если максимальная длина блока превышает их. Вы также можете использовать специальные разделители, определенные вами (например, ***).',
},
stepThree: {
creationTitle: '🎉 База знаний создана',
diff --git a/web/i18n/tr-TR/dataset-creation.ts b/web/i18n/tr-TR/dataset-creation.ts
index b26608c39f..c29e3045b8 100644
--- a/web/i18n/tr-TR/dataset-creation.ts
+++ b/web/i18n/tr-TR/dataset-creation.ts
@@ -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',
diff --git a/web/i18n/uk-UA/dataset-creation.ts b/web/i18n/uk-UA/dataset-creation.ts
index e4a38f41f4..5b2c9503cf 100644
--- a/web/i18n/uk-UA/dataset-creation.ts
+++ b/web/i18n/uk-UA/dataset-creation.ts
@@ -133,6 +133,7 @@ const translation = {
datasetSettingLink: 'Налаштування знань.',
webpageUnit: 'Сторінок',
websiteSource: 'Веб-сайт попередньої обробки',
+ separatorTip: 'Роздільник – це символ, який використовується для поділу тексту. \\n\\n та \\n є часто використовуваними роздільниками для відокремлення абзаців та рядків. У поєднанні з комами (\\n\\n,\\n) абзаци будуть розділені лініями, якщо вони перевищують максимальну довжину фрагмента. Ви також можете використовувати спеціальні роздільники, визначені вами (наприклад, ***).',
},
stepThree: {
creationTitle: '🎉 Знання створено',
diff --git a/web/i18n/vi-VN/dataset-creation.ts b/web/i18n/vi-VN/dataset-creation.ts
index da69020287..af49575b90 100644
--- a/web/i18n/vi-VN/dataset-creation.ts
+++ b/web/i18n/vi-VN/dataset-creation.ts
@@ -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',
diff --git a/web/i18n/zh-Hans/app-api.ts b/web/i18n/zh-Hans/app-api.ts
index 6b9048b66e..a0defdab62 100644
--- a/web/i18n/zh-Hans/app-api.ts
+++ b/web/i18n/zh-Hans/app-api.ts
@@ -6,6 +6,7 @@ const translation = {
ok: '运行中',
copy: '复制',
copied: '已复制',
+ regenerate: '重新生成',
play: '播放',
pause: '暂停',
playing: '播放中',
diff --git a/web/i18n/zh-Hant/dataset-creation.ts b/web/i18n/zh-Hant/dataset-creation.ts
index fd810d41c1..73a57db6a0 100644
--- a/web/i18n/zh-Hant/dataset-creation.ts
+++ b/web/i18n/zh-Hant/dataset-creation.ts
@@ -133,6 +133,7 @@ const translation = {
datasetSettingLink: '知識庫設定。',
websiteSource: '預處理網站',
webpageUnit: '頁面',
+ separatorTip: '分隔符是用於分隔文字的字元。\\n\\n 和 \\n 是分隔段落和行的常用分隔符。與逗號 (\\n\\n,\\n) 組合使用時,當超過最大區塊長度時,段落將按行分段。您也可以使用自定義的特殊分隔符(例如 ***)。',
},
stepThree: {
creationTitle: '🎉 知識庫已建立',
diff --git a/web/models/log.ts b/web/models/log.ts
index 8da1c4cf4e..dc557bfe21 100644
--- a/web/models/log.ts
+++ b/web/models/log.ts
@@ -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 = {
diff --git a/web/package.json b/web/package.json
index bc532fb242..b775d87184 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",