Merge branch 'feat/rag-pipeline' of https://github.com/langgenius/dify into feat/rag-pipeline

This commit is contained in:
twwu
2025-04-30 14:16:25 +08:00
321 changed files with 5480 additions and 1029 deletions

View File

@ -39,6 +39,7 @@ export type EmbeddedChatbotContextValue = {
chatShouldReloadKey: string
isMobile: boolean
isInstalledApp: boolean
allowResetChat: boolean
appId?: string
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
@ -67,6 +68,7 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
chatShouldReloadKey: '',
isMobile: false,
isInstalledApp: false,
allowResetChat: true,
handleFeedback: noop,
currentChatInstanceRef: { current: { handleStop: noop } },
clearChatList: false,

View File

@ -1,6 +1,6 @@
import type { FC } from 'react'
import React from 'react'
import { RiResetLeftLine } from '@remixicon/react'
import React, { useCallback, useEffect, useState } from 'react'
import { RiCollapseDiagonal2Line, RiExpandDiagonal2Line, RiResetLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
@ -16,6 +16,7 @@ import cn from '@/utils/classnames'
export type IHeaderProps = {
isMobile?: boolean
allowResetChat?: boolean
customerIcon?: React.ReactNode
title: string
theme?: Theme
@ -23,6 +24,7 @@ export type IHeaderProps = {
}
const Header: FC<IHeaderProps> = ({
isMobile,
allowResetChat,
customerIcon,
title,
theme,
@ -34,6 +36,44 @@ const Header: FC<IHeaderProps> = ({
currentConversationId,
inputsForms,
} = useEmbeddedChatbotContext()
const isClient = typeof window !== 'undefined'
const isIframe = isClient ? window.self !== window.top : false
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin
if (!currentParentOrigin && event.data.type === 'dify-chatbot-config') {
currentParentOrigin = event.origin
setParentOrigin(event.origin)
}
if (event.origin !== currentParentOrigin)
return
if (event.data.type === 'dify-chatbot-config')
setShowToggleExpandButton(event.data.payload.isToggledByButton && !event.data.payload.isDraggable)
}, [parentOrigin])
useEffect(() => {
if (!isIframe) return
const listener = (event: MessageEvent) => handleMessageReceived(event)
window.addEventListener('message', listener)
window.parent.postMessage({ type: 'dify-chatbot-iframe-ready' }, '*')
return () => window.removeEventListener('message', listener)
}, [isIframe, handleMessageReceived])
const handleToggleExpand = useCallback(() => {
if (!isIframe || !showToggleExpandButton) return
setExpanded(!expanded)
window.parent.postMessage({
type: 'dify-chatbot-expand-change',
}, parentOrigin)
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
if (!isMobile) {
return (
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
@ -57,7 +97,22 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && (
<Divider type='vertical' className='h-3.5' />
)}
{currentConversationId && (
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className='h-[18px] w-[18px]' />
: <RiExpandDiagonal2Line className='h-[18px] w-[18px]' />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>
@ -89,7 +144,22 @@ const Header: FC<IHeaderProps> = ({
</div>
</div>
<div className='flex items-center gap-1'>
{currentConversationId && (
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
: <RiExpandDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}
>

View File

@ -73,9 +73,11 @@ export const useEmbeddedChatbot = () => {
const appId = useMemo(() => appData?.app_id, [appData])
const [userId, setUserId] = useState<string>()
const [conversationId, setConversationId] = useState<string>()
useEffect(() => {
getProcessedSystemVariablesFromUrlParams().then(({ user_id }) => {
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
setUserId(user_id)
setConversationId(conversation_id)
})
}, [])
@ -109,7 +111,9 @@ export const useEmbeddedChatbot = () => {
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
defaultValue: {},
})
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId])
const allowResetChat = !conversationId
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
[appId, conversationIdInfo, userId, conversationId])
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
if (appId) {
let prevValue = conversationIdInfo?.[appId || '']
@ -362,6 +366,7 @@ export const useEmbeddedChatbot = () => {
appInfoError,
appInfoLoading,
isInstalledApp,
allowResetChat,
appId,
currentConversationId,
currentConversationItem,

View File

@ -25,6 +25,7 @@ import cn from '@/utils/classnames'
const Chatbot = () => {
const {
isMobile,
allowResetChat,
appInfoError,
appInfoLoading,
appData,
@ -90,6 +91,7 @@ const Chatbot = () => {
>
<Header
isMobile={isMobile}
allowResetChat={allowResetChat}
title={site?.title || ''}
customerIcon={isDify() ? difyIcon : ''}
theme={themeBuilder?.theme}
@ -153,6 +155,7 @@ const EmbeddedChatbotWrapper = () => {
handleNewConversationCompleted,
chatShouldReloadKey,
isInstalledApp,
allowResetChat,
appId,
handleFeedback,
currentChatInstanceRef,
@ -187,6 +190,7 @@ const EmbeddedChatbotWrapper = () => {
chatShouldReloadKey,
isMobile,
isInstalledApp,
allowResetChat,
appId,
handleFeedback,
currentChatInstanceRef,

View File

@ -0,0 +1,58 @@
import { useState } from 'react'
import { RiEditLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import SegmentedControl from '@/app/components/base/segmented-control'
import { VariableX } from '@/app/components/base/icons/src/vender/workflow'
import type { LabelProps } from '../label'
import Label from '../label'
type VariableOrConstantInputFieldProps = {
label: string
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
className?: string
}
const VariableOrConstantInputField = ({
className,
label,
labelOptions,
}: VariableOrConstantInputFieldProps) => {
const [variableType, setVariableType] = useState('variable')
const options = [
{
Icon: VariableX,
text: '',
value: 'variable',
},
{
Icon: RiEditLine,
text: '',
value: 'constant',
},
]
const handleVariableOrConstantChange = (value: string) => {
setVariableType(value)
}
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={'variable-or-constant'}
label={label}
{...(labelOptions ?? {})}
/>
<div className='flex items-center'>
<SegmentedControl
className='mr-1 shrink-0'
value={variableType}
onChange={handleVariableOrConstantChange as any}
options={options as any}
/>
</div>
</div>
)
}
export default VariableOrConstantInputField

View File

@ -0,0 +1,239 @@
import React from 'react'
import { type InputFieldConfiguration, InputFieldType } from './types'
import { withForm } from '../..'
import { useStore } from '@tanstack/react-form'
type InputFieldProps<T> = {
initialData?: T
config: InputFieldConfiguration<T>
}
const NodePanelField = <T,>({
initialData,
config,
}: InputFieldProps<T>) => withForm({
defaultValues: initialData,
render: function Render({
form,
}) {
const {
type,
label,
placeholder,
variable,
tooltip,
showConditions,
max,
min,
required,
showOptional,
supportFile,
description,
options,
listeners,
popupProps,
} = config
const isAllConditionsMet = useStore(form.store, (state) => {
const fieldValues = state.values
return showConditions.every((condition) => {
const { variable, value } = condition
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
return fieldValue === value
})
})
if (!isAllConditionsMet)
return <></>
if (type === InputFieldType.textInput) {
return (
<form.AppField
name={variable}
children={field => (
<field.TextField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
placeholder={placeholder}
/>
)}
/>
)
}
if (type === InputFieldType.numberInput) {
return (
<form.AppField
name={variable}
children={field => (
<field.NumberInputField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
placeholder={placeholder}
max={max}
min={min}
/>
)}
/>
)
}
if (type === InputFieldType.numberSlider) {
return (
<form.AppField
name={variable}
children={field => (
<field.NumberSliderField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
description={description}
max={max}
min={min}
/>
)}
/>
)
}
if (type === InputFieldType.checkbox) {
return (
<form.AppField
name={variable}
children={field => (
<field.CheckboxField
label={label}
/>
)}
/>
)
}
if (type === InputFieldType.select) {
return (
<form.AppField
name={variable}
children={field => (
<field.SelectField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
options={options!}
popupProps={popupProps}
/>
)}
/>
)
}
if (type === InputFieldType.inputTypeSelect) {
return (
<form.AppField
name={variable}
listeners={listeners}
children={field => (
<field.InputTypeSelectField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
supportFile={!!supportFile}
/>
)}
/>
)
}
if (type === InputFieldType.uploadMethod) {
return (
<form.AppField
name={variable}
children={field => (
<field.UploadMethodField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
/>
)}
/>
)
}
if (type === InputFieldType.fileTypes) {
return (
<form.AppField
name={variable}
children={field => (
<field.FileTypesField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
/>
)}
/>
)
}
if (type === InputFieldType.options) {
return (
<form.AppField
name={variable}
children={field => (
<field.OptionsField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
/>
)}
/>
)
}
if (type === InputFieldType.variableOrConstant) {
return (
<form.AppField
name={variable}
children={field => (
<field.VariableOrConstantInputField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
/>
)}
/>
)
}
return <></>
},
})
export default NodePanelField

View File

@ -0,0 +1,40 @@
import type { DeepKeys, FieldListeners } from '@tanstack/react-form'
import type { NumberConfiguration, SelectConfiguration, ShowCondition } from '../base/types'
export enum InputFieldType {
textInput = 'textInput',
numberInput = 'numberInput',
numberSlider = 'numberSlider',
checkbox = 'checkbox',
options = 'options',
select = 'select',
inputTypeSelect = 'inputTypeSelect',
uploadMethod = 'uploadMethod',
fileTypes = 'fileTypes',
variableOrConstant = 'variableOrConstant',
}
export type InputTypeSelectConfiguration = {
supportFile: boolean
}
export type NumberSliderConfiguration = {
description: string
max?: number
min?: number
}
export type InputFieldConfiguration<T> = {
label: string
variable: DeepKeys<T> // Variable name
maxLength?: number // Max length for text input
placeholder?: string
required: boolean
showOptional?: boolean // show optional label
showConditions: ShowCondition<T>[] // Show this field only when all conditions are met
type: InputFieldType
tooltip?: string // Tooltip for this field
listeners?: FieldListeners<T, DeepKeys<T>> // Listener for this field
} & NumberConfiguration & Partial<InputTypeSelectConfiguration>
& Partial<NumberSliderConfiguration>
& Partial<SelectConfiguration>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './WeaveIcon.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 = 'WeaveIcon'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './WeaveIconBig.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 = 'WeaveIconBig'
export default Icon

View File

@ -5,3 +5,5 @@ export { default as LangsmithIcon } from './LangsmithIcon'
export { default as OpikIconBig } from './OpikIconBig'
export { default as OpikIcon } from './OpikIcon'
export { default as TracingIcon } from './TracingIcon'
export { default as WeaveIconBig } from './WeaveIconBig'
export { default as WeaveIcon } from './WeaveIcon'

View File

@ -1,4 +1,5 @@
import type { FC } from 'react'
import { basePath } from '@/utils/var'
type LogoEmbeddedChatAvatarProps = {
className?: string
@ -8,7 +9,7 @@ const LogoEmbeddedChatAvatar: FC<LogoEmbeddedChatAvatarProps> = ({
}) => {
return (
<img
src='/logo/logo-embedded-chat-avatar.png'
src={`${basePath}/logo/logo-embedded-chat-avatar.png`}
className={`block h-10 w-10 ${className}`}
alt='logo'
/>

View File

@ -22,10 +22,6 @@ export function preprocessMermaidCode(code: string): string {
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`)
// Fix common syntax issues
.replace(/fifopacket/g, 'rect')
// Ensure graph has direction
.replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
return direction ? match : 'graph TD'
})
// Clean up empty lines and extra spaces
.trim()
}

View File

@ -144,7 +144,7 @@ const WorkflowVariableBlockComponent = ({
}
if (!node)
return null
return Item
return (
<Tooltip

View File

@ -68,7 +68,7 @@ const Toast = ({
</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} grow flex-col items-start gap-1`}>
<div className='flex items-center gap-1'>
<div className='system-sm-semibold text-text-primary'>{message}</div>
<div className='system-sm-semibold text-text-primary [word-break:break-word]'>{message}</div>
{customComponent}
</div>
{children && <div className='system-xs-regular text-text-secondary'>