Merge branch 'main' into feat/rag-pipeline

This commit is contained in:
zxhlyh
2025-04-29 16:27:49 +08:00
279 changed files with 4905 additions and 859 deletions

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import TracingIcon from './tracing-icon'
import ProviderPanel from './provider-panel'
import type { LangFuseConfig, LangSmithConfig, OpikConfig } from './type'
import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import ProviderConfigModal from './provider-config-modal'
import Indicator from '@/app/components/header/indicator'
@ -26,7 +26,8 @@ export type PopupProps = {
langSmithConfig: LangSmithConfig | null
langFuseConfig: LangFuseConfig | null
opikConfig: OpikConfig | null
onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig | OpikConfig) => void
weaveConfig: WeaveConfig | null
onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void
onConfigRemoved: (provider: TracingProvider) => void
}
@ -40,6 +41,7 @@ const ConfigPopup: FC<PopupProps> = ({
langSmithConfig,
langFuseConfig,
opikConfig,
weaveConfig,
onConfigUpdated,
onConfigRemoved,
}) => {
@ -63,7 +65,7 @@ const ConfigPopup: FC<PopupProps> = ({
}
}, [onChooseProvider])
const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig | OpikConfig) => {
const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => {
onConfigUpdated(currentProvider!, payload)
hideConfigModal()
}, [currentProvider, hideConfigModal, onConfigUpdated])
@ -73,8 +75,8 @@ const ConfigPopup: FC<PopupProps> = ({
hideConfigModal()
}, [currentProvider, hideConfigModal, onConfigRemoved])
const providerAllConfigured = langSmithConfig && langFuseConfig && opikConfig
const providerAllNotConfigured = !langSmithConfig && !langFuseConfig && !opikConfig
const providerAllConfigured = langSmithConfig && langFuseConfig && opikConfig && weaveConfig
const providerAllNotConfigured = !langSmithConfig && !langFuseConfig && !opikConfig && !weaveConfig
const switchContent = (
<Switch
@ -123,33 +125,51 @@ const ConfigPopup: FC<PopupProps> = ({
/>
)
const weavePanel = (
<ProviderPanel
type={TracingProvider.weave}
readOnly={readOnly}
config={weaveConfig}
hasConfigured={!!weaveConfig}
onConfig={handleOnConfig(TracingProvider.weave)}
isChosen={chosenProvider === TracingProvider.weave}
onChoose={handleOnChoose(TracingProvider.weave)}
key="weave-provider-panel"
/>
)
const configuredProviderPanel = () => {
const configuredPanels: JSX.Element[] = []
if (langSmithConfig)
configuredPanels.push(langSmithPanel)
if (langFuseConfig)
configuredPanels.push(langfusePanel)
if (langSmithConfig)
configuredPanels.push(langSmithPanel)
if (opikConfig)
configuredPanels.push(opikPanel)
if (weaveConfig)
configuredPanels.push(weavePanel)
return configuredPanels
}
const moreProviderPanel = () => {
const notConfiguredPanels: JSX.Element[] = []
if (!langSmithConfig)
notConfiguredPanels.push(langSmithPanel)
if (!langFuseConfig)
notConfiguredPanels.push(langfusePanel)
if (!langSmithConfig)
notConfiguredPanels.push(langSmithPanel)
if (!opikConfig)
notConfiguredPanels.push(opikPanel)
if (!weaveConfig)
notConfiguredPanels.push(weavePanel)
return notConfiguredPanels
}
@ -158,7 +178,9 @@ const ConfigPopup: FC<PopupProps> = ({
return langSmithConfig
if (currentProvider === TracingProvider.langfuse)
return langFuseConfig
return opikConfig
if (currentProvider === TracingProvider.opik)
return opikConfig
return weaveConfig
}
return (
@ -199,9 +221,10 @@ const ConfigPopup: FC<PopupProps> = ({
<>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className='mt-2 space-y-2'>
{langSmithPanel}
{langfusePanel}
{langSmithPanel}
{opikPanel}
{weavePanel}
</div>
</>
)

View File

@ -4,4 +4,5 @@ export const docURL = {
[TracingProvider.langSmith]: 'https://docs.smith.langchain.com/',
[TracingProvider.langfuse]: 'https://docs.langfuse.com',
[TracingProvider.opik]: 'https://www.comet.com/docs/opik/tracing/integrations/dify#setup-instructions',
[TracingProvider.weave]: 'https://weave-docs.wandb.ai/',
}

View File

@ -7,12 +7,12 @@ import {
import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation'
import { useBoolean } from 'ahooks'
import type { LangFuseConfig, LangSmithConfig, OpikConfig } from './type'
import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import TracingIcon from './tracing-icon'
import ConfigButton from './config-button'
import cn from '@/utils/classnames'
import { LangfuseIcon, LangsmithIcon, OpikIcon } from '@/app/components/base/icons/src/public/tracing'
import { LangfuseIcon, LangsmithIcon, OpikIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing'
import Indicator from '@/app/components/header/indicator'
import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
import type { TracingStatus } from '@/models/app'
@ -82,12 +82,15 @@ const Panel: FC = () => {
? LangfuseIcon
: inUseTracingProvider === TracingProvider.opik
? OpikIcon
: LangsmithIcon
: inUseTracingProvider === TracingProvider.weave
? WeaveIcon
: LangsmithIcon
const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
const [opikConfig, setOpikConfig] = useState<OpikConfig | null>(null)
const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig)
const [weaveConfig, setWeaveConfig] = useState<WeaveConfig | null>(null)
const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig)
const fetchTracingConfig = async () => {
const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith })
@ -99,6 +102,9 @@ const Panel: FC = () => {
const { tracing_config: opikConfig, has_not_configured: OpikHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.opik })
if (!OpikHasNotConfig)
setOpikConfig(opikConfig as OpikConfig)
const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave })
if (!weaveHasNotConfig)
setWeaveConfig(weaveConfig as WeaveConfig)
}
const handleTracingConfigUpdated = async (provider: TracingProvider) => {
@ -110,6 +116,8 @@ const Panel: FC = () => {
setLangFuseConfig(tracing_config as LangFuseConfig)
else if (provider === TracingProvider.opik)
setOpikConfig(tracing_config as OpikConfig)
else if (provider === TracingProvider.weave)
setWeaveConfig(tracing_config as WeaveConfig)
}
const handleTracingConfigRemoved = (provider: TracingProvider) => {
@ -119,6 +127,8 @@ const Panel: FC = () => {
setLangFuseConfig(null)
else if (provider === TracingProvider.opik)
setOpikConfig(null)
else if (provider === TracingProvider.weave)
setWeaveConfig(null)
if (provider === inUseTracingProvider) {
handleTracingStatusChange({
enabled: false,
@ -178,6 +188,7 @@ const Panel: FC = () => {
langSmithConfig={langSmithConfig}
langFuseConfig={langFuseConfig}
opikConfig={opikConfig}
weaveConfig={weaveConfig}
onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup}
@ -212,6 +223,7 @@ const Panel: FC = () => {
langSmithConfig={langSmithConfig}
langFuseConfig={langFuseConfig}
opikConfig={opikConfig}
weaveConfig={weaveConfig}
onConfigUpdated={handleTracingConfigUpdated}
onConfigRemoved={handleTracingConfigRemoved}
controlShowPopup={controlShowPopup}

View File

@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import Field from './field'
import type { LangFuseConfig, LangSmithConfig, OpikConfig } from './type'
import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type'
import { TracingProvider } from './type'
import { docURL } from './config'
import {
@ -22,10 +22,10 @@ import Divider from '@/app/components/base/divider'
type Props = {
appId: string
type: TracingProvider
payload?: LangSmithConfig | LangFuseConfig | OpikConfig | null
payload?: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | null
onRemoved: () => void
onCancel: () => void
onSaved: (payload: LangSmithConfig | LangFuseConfig | OpikConfig) => void
onSaved: (payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void
onChosen: (provider: TracingProvider) => void
}
@ -50,6 +50,13 @@ const opikConfigTemplate = {
workspace: '',
}
const weaveConfigTemplate = {
api_key: '',
entity: '',
project: '',
endpoint: '',
}
const ProviderConfigModal: FC<Props> = ({
appId,
type,
@ -63,7 +70,7 @@ const ProviderConfigModal: FC<Props> = ({
const isEdit = !!payload
const isAdd = !isEdit
const [isSaving, setIsSaving] = useState(false)
const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig | OpikConfig>((() => {
const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig>((() => {
if (isEdit)
return payload
@ -73,7 +80,10 @@ const ProviderConfigModal: FC<Props> = ({
else if (type === TracingProvider.langfuse)
return langFuseConfigTemplate
return opikConfigTemplate
else if (type === TracingProvider.opik)
return opikConfigTemplate
return weaveConfigTemplate
})())
const [isShowRemoveConfirm, {
setTrue: showRemoveConfirm,
@ -127,6 +137,14 @@ const ProviderConfigModal: FC<Props> = ({
// const postData = config as OpikConfig
}
if (type === TracingProvider.weave) {
const postData = config as WeaveConfig
if (!errorMessage && !postData.api_key)
errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
if (!errorMessage && !postData.project)
errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
}
return errorMessage
}, [config, t, type])
const handleSave = useCallback(async () => {
@ -176,6 +194,40 @@ const ProviderConfigModal: FC<Props> = ({
</div>
<div className='space-y-4'>
{type === TracingProvider.weave && (
<>
<Field
label='API Key'
labelClassName='!text-sm'
isRequired
value={(config as WeaveConfig).api_key}
onChange={handleConfigChange('api_key')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
/>
<Field
label={t(`${I18N_PREFIX}.project`)!}
labelClassName='!text-sm'
isRequired
value={(config as WeaveConfig).project}
onChange={handleConfigChange('project')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
/>
<Field
label='Entity'
labelClassName='!text-sm'
value={(config as WeaveConfig).entity}
onChange={handleConfigChange('entity')}
placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Entity' })!}
/>
<Field
label='Endpoint'
labelClassName='!text-sm'
value={(config as WeaveConfig).endpoint}
onChange={handleConfigChange('endpoint')}
placeholder={'https://trace.wandb.ai/'}
/>
</>
)}
{type === TracingProvider.langSmith && (
<>
<Field
@ -263,7 +315,6 @@ const ProviderConfigModal: FC<Props> = ({
/>
</>
)}
</div>
<div className='my-8 flex h-8 items-center justify-between'>
<a

View File

@ -7,7 +7,7 @@ import {
import { useTranslation } from 'react-i18next'
import { TracingProvider } from './type'
import cn from '@/utils/classnames'
import { LangfuseIconBig, LangsmithIconBig, OpikIconBig } from '@/app/components/base/icons/src/public/tracing'
import { LangfuseIconBig, LangsmithIconBig, OpikIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing'
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
const I18N_PREFIX = 'app.tracing'
@ -27,6 +27,7 @@ const getIcon = (type: TracingProvider) => {
[TracingProvider.langSmith]: LangsmithIconBig,
[TracingProvider.langfuse]: LangfuseIconBig,
[TracingProvider.opik]: OpikIconBig,
[TracingProvider.weave]: WeaveIconBig,
})[type]
}

View File

@ -2,6 +2,7 @@ export enum TracingProvider {
langSmith = 'langsmith',
langfuse = 'langfuse',
opik = 'opik',
weave = 'weave',
}
export type LangSmithConfig = {
@ -22,3 +23,10 @@ export type OpikConfig = {
workspace: string
url: string
}
export type WeaveConfig = {
api_key: string
entity: string
project: string
endpoint: string
}

View File

@ -231,7 +231,7 @@ const AppPublisher = ({
>
{t('workflow.common.runApp')}
</SuggestedAction>
{appDetail?.mode === 'workflow'
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
? (
<SuggestedAction
disabled={!publishedAt}

View File

@ -14,6 +14,7 @@ import Loading from '@/app/components/base/loading'
import Badge from '@/app/components/base/badge'
import { useKnowledge } from '@/hooks/use-knowledge'
import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
export type ISelectDataSetProps = {
isShow: boolean
@ -111,7 +112,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}}
>
<span className='text-text-tertiary'>{t('appDebug.feature.dataSet.noDataSet')}</span>
<Link href="/datasets/create" className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link>
<Link href={`${basePath}/datasets/create`} className='font-normal text-text-accent'>{t('appDebug.feature.dataSet.toCreate')}</Link>
</div>
)}

View File

@ -4,7 +4,6 @@ import { useMount } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { isEqual } from 'lodash-es'
import { RiCloseLine } from '@remixicon/react'
import { BookOpenIcon } from '@heroicons/react/24/outline'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import cn from '@/utils/classnames'
import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
@ -223,10 +222,6 @@ const SettingsModal: FC<SettingsModalProps> = ({
className='resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
/>
<a className='mt-2 flex h-[18px] items-center px-3 text-xs text-text-tertiary' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>
<BookOpenIcon className='mr-1 h-[18px] w-3' />
{t('datasetSettings.form.descWrite')}
</a>
</div>
</div>
<div className={rowClass}>

View File

@ -191,14 +191,16 @@ const Apps = ({
</div>
<div className='relative flex flex-1 overflow-y-auto'>
{!searchKeywords && <div className='h-full w-[200px] p-4'>
<Sidebar current={currCategory as AppCategories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
<Sidebar current={currCategory as AppCategories} categories={categories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
</div>}
<div className='h-full flex-1 shrink-0 grow overflow-auto border-l border-divider-burn p-6 pt-2'>
{searchFilteredList && searchFilteredList.length > 0 && <>
<div className='pb-1 pt-4'>
{searchKeywords
? <p className='title-md-semi-bold text-text-tertiary'>{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}</p>
: <AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />}
: <div className='flex h-[22px] items-center'>
<AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />
</div>}
</div>
<div
className={cn(

View File

@ -1,39 +1,29 @@
'use client'
import { RiAppsFill, RiChatSmileAiFill, RiExchange2Fill, RiPassPendingFill, RiQuillPenAiFill, RiSpeakAiFill, RiStickyNoteAddLine, RiTerminalBoxFill, RiThumbUpFill } from '@remixicon/react'
import { RiStickyNoteAddLine, RiThumbUpLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
export enum AppCategories {
RECOMMENDED = 'Recommended',
ASSISTANT = 'Assistant',
AGENT = 'Agent',
HR = 'HR',
PROGRAMMING = 'Programming',
WORKFLOW = 'Workflow',
WRITING = 'Writing',
}
type SidebarProps = {
current: AppCategories
onClick?: (category: AppCategories) => void
current: AppCategories | string
categories: string[]
onClick?: (category: AppCategories | string) => void
onCreateFromBlank?: () => void
}
export default function Sidebar({ current, onClick, onCreateFromBlank }: SidebarProps) {
export default function Sidebar({ current, categories, onClick, onCreateFromBlank }: SidebarProps) {
const { t } = useTranslation()
return <div className="flex h-full w-full flex-col">
<ul>
<ul className='pt-0.5'>
<CategoryItem category={AppCategories.RECOMMENDED} active={current === AppCategories.RECOMMENDED} onClick={onClick} />
</ul>
<div className='system-xs-medium-uppercase px-3 pb-1 pt-2 text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
<div className='system-xs-medium-uppercase mb-0.5 mt-3 px-3 pb-1 pt-2 text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
<ul className='flex grow flex-col gap-0.5'>
<CategoryItem category={AppCategories.ASSISTANT} active={current === AppCategories.ASSISTANT} onClick={onClick} />
<CategoryItem category={AppCategories.AGENT} active={current === AppCategories.AGENT} onClick={onClick} />
<CategoryItem category={AppCategories.HR} active={current === AppCategories.HR} onClick={onClick} />
<CategoryItem category={AppCategories.PROGRAMMING} active={current === AppCategories.PROGRAMMING} onClick={onClick} />
<CategoryItem category={AppCategories.WORKFLOW} active={current === AppCategories.WORKFLOW} onClick={onClick} />
<CategoryItem category={AppCategories.WRITING} active={current === AppCategories.WRITING} onClick={onClick} />
{categories.map(category => (<CategoryItem key={category} category={category} active={current === category} onClick={onClick} />))}
</ul>
<Divider bgStyle='gradient' />
<div className='flex cursor-pointer items-center gap-1 px-3 py-1 text-text-tertiary' onClick={onCreateFromBlank}>
@ -45,47 +35,26 @@ export default function Sidebar({ current, onClick, onCreateFromBlank }: Sidebar
type CategoryItemProps = {
active: boolean
category: AppCategories
onClick?: (category: AppCategories) => void
category: AppCategories | string
onClick?: (category: AppCategories | string) => void
}
function CategoryItem({ category, active, onClick }: CategoryItemProps) {
return <li
className={classNames('p-1 pl-3 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
className={classNames('p-1 pl-3 h-8 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
onClick={() => { onClick?.(category) }}>
<div className='inline-flex h-5 w-5 items-center justify-center rounded-md border border-divider-regular bg-components-icon-bg-midnight-solid group-[.active]:bg-components-icon-bg-blue-solid'>
<AppCategoryIcon category={category} />
</div>
{category === AppCategories.RECOMMENDED && <div className='inline-flex h-5 w-5 items-center justify-center rounded-md'>
<RiThumbUpLine className='h-4 w-4 text-components-menu-item-text group-[.active]:text-components-menu-item-text-active' />
</div>}
<AppCategoryLabel category={category}
className={classNames('system-sm-medium text-components-menu-item-text group-[.active]:text-components-menu-item-text-active group-hover:text-components-menu-item-text-hover', active && 'system-sm-semibold')} />
</li >
}
type AppCategoryLabelProps = {
category: AppCategories
category: AppCategories | string
className?: string
}
export function AppCategoryLabel({ category, className }: AppCategoryLabelProps) {
const { t } = useTranslation()
return <span className={className}>{t(`app.newAppFromTemplate.sidebar.${category}`)}</span>
}
type AppCategoryIconProps = {
category: AppCategories
}
function AppCategoryIcon({ category }: AppCategoryIconProps) {
if (category === AppCategories.AGENT)
return <RiSpeakAiFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.ASSISTANT)
return <RiChatSmileAiFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.HR)
return <RiPassPendingFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.PROGRAMMING)
return <RiTerminalBoxFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.RECOMMENDED)
return <RiThumbUpFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.WRITING)
return <RiQuillPenAiFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
if (category === AppCategories.WORKFLOW)
return <RiExchange2Fill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
return <RiAppsFill className='h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100' />
return <span className={className}>{category === AppCategories.RECOMMENDED ? t('app.newAppFromTemplate.sidebar.Recommended') : category}</span>
}

View File

@ -0,0 +1,46 @@
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
type DSLConfirmModalProps = {
versions?: {
importedVersion: string
systemVersion: string
}
onCancel: () => void
onConfirm: () => void
confirmDisabled?: boolean
}
const DSLConfirmModal = ({
versions = { importedVersion: '', systemVersion: '' },
onCancel,
onConfirm,
confirmDisabled = false,
}: DSLConfirmModalProps) => {
const { t } = useTranslation()
return (
<Modal
isShow
onClose={() => onCancel()}
className='w-[480px]'
>
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
<div className='system-md-regular flex grow flex-col text-text-secondary'>
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
<br />
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div>
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions.systemVersion}</span></div>
</div>
</div>
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
<Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button>
</div>
</Modal>
)
}
export default DSLConfirmModal

View File

@ -29,7 +29,7 @@ const OPTION_MAP = {
iframe: {
getContent: (url: string, token: string) =>
`<iframe
src="${url}${basePath}/chat/${token}"
src="${url}${basePath}/chatbot/${token}"
style="width: 100%; height: 100%; min-height: 700px"
frameborder="0"
allow="microphone">
@ -48,6 +48,7 @@ const OPTION_MAP = {
: ''},
systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
},
}
</script>

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,

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'>

View File

@ -1,3 +1,4 @@
import type { BasicPlan } from '@/app/components/billing/type'
import { Plan, type PlanInfo, Priority } from '@/app/components/billing/type'
const supportModelProviders = 'OpenAI/Anthropic/Llama2/Azure OpenAI/Hugging Face/Replicate'
@ -10,7 +11,7 @@ export const contactSalesUrl = 'https://vikgc6bnu1s.typeform.com/dify-business'
export const getStartedWithCommunityUrl = 'https://github.com/langgenius/dify'
export const getWithPremiumUrl = 'https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6'
export const ALL_PLANS: Record<Plan, PlanInfo> = {
export const ALL_PLANS: Record<BasicPlan, PlanInfo> = {
sandbox: {
level: 1,
price: 0,
@ -22,6 +23,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '50MB',
documentsUploadQuota: 0,
documentsRequestQuota: 10,
apiRateLimit: 5000,
documentProcessingPriority: Priority.standard,
messageRequest: 200,
annotatedResponse: 10,
@ -38,6 +40,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '5GB',
documentsUploadQuota: 0,
documentsRequestQuota: 100,
apiRateLimit: NUM_INFINITE,
documentProcessingPriority: Priority.priority,
messageRequest: 5000,
annotatedResponse: 2000,
@ -54,6 +57,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
vectorSpace: '20GB',
documentsUploadQuota: 0,
documentsRequestQuota: 1000,
apiRateLimit: NUM_INFINITE,
documentProcessingPriority: Priority.topPriority,
messageRequest: 10000,
annotatedResponse: 5000,
@ -62,7 +66,7 @@ export const ALL_PLANS: Record<Plan, PlanInfo> = {
}
export const defaultPlan = {
type: Plan.sandbox,
type: Plan.sandbox as BasicPlan,
usage: {
documents: 50,
vectorSpace: 1,

View File

@ -2,7 +2,8 @@
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine } from '@remixicon/react'
import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine, RiTerminalBoxLine } from '@remixicon/react'
import type { BasicPlan } from '../type'
import { Plan } from '../type'
import { ALL_PLANS, NUM_INFINITE } from '../config'
import Toast from '../../base/toast'
@ -15,8 +16,8 @@ import { useAppContext } from '@/context/app-context'
import { fetchSubscriptionUrls } from '@/service/billing'
type Props = {
currentPlan: Plan
plan: Plan
currentPlan: BasicPlan
plan: BasicPlan
planRange: PlanRange
canPay: boolean
}
@ -127,8 +128,8 @@ const PlanItem: FC<Props> = ({
<div className='flex flex-col gap-y-1'>
{style[plan].icon}
<div className='flex items-center'>
<div className='text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 flex items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
<div className='grow text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
{isMostPopularPlan && <div className='ml-1 flex shrink-0 items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
<div className='pl-0.5'>
<SparklesSoft className='size-3' />
</div>
@ -205,6 +206,14 @@ const PlanItem: FC<Props> = ({
label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
/>
<KeyValue
icon={<RiTerminalBoxLine />}
label={
planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
: `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
}
tooltip={planInfo.apiRateLimit === NUM_INFINITE ? null : t('billing.plansCommon.apiRateLimitTooltip') as string}
/>
<KeyValue
icon={<RiProgress3Line />}
label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}

View File

@ -9,6 +9,9 @@ export enum Priority {
priority = 'priority',
topPriority = 'top-priority',
}
export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team
export type PlanInfo = {
level: number
price: number
@ -20,6 +23,7 @@ export type PlanInfo = {
vectorSpace: string
documentsUploadQuota: number
documentsRequestQuota: number
apiRateLimit: number
documentProcessingPriority: Priority
logHistory: number
messageRequest: number
@ -60,7 +64,7 @@ export type CurrentPlanInfoBackend = {
billing: {
enabled: boolean
subscription: {
plan: Plan
plan: BasicPlan
}
}
members: {

View File

@ -196,22 +196,68 @@ const FileUploader = ({
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
}
type FileWithPath = {
relativePath?: string
} & File
const traverseFileEntry = useCallback(
(entry: any, prefix = ''): Promise<FileWithPath[]> => {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file: FileWithPath) => {
file.relativePath = `${prefix}${file.name}`
resolve([file])
})
}
else if (entry.isDirectory) {
const reader = entry.createReader()
const entries: any[] = []
const read = () => {
reader.readEntries(async (results: FileSystemEntry[]) => {
if (!results.length) {
const files = await Promise.all(
entries.map(ent =>
traverseFileEntry(ent, `${prefix}${entry.name}/`),
),
)
resolve(files.flat())
}
else {
entries.push(...results)
read()
}
})
}
read()
}
else {
resolve([])
}
})
},
[],
)
const handleDrop = useCallback((e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer)
return
let files = [...e.dataTransfer.files] as File[]
if (notSupportBatchUpload)
files = files.slice(0, 1)
const validFiles = files.filter(isValid)
initialUpload(validFiles)
}, [initialUpload, isValid, notSupportBatchUpload])
const handleDrop = useCallback(
async (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer) return
const nested = await Promise.all(
Array.from(e.dataTransfer.items).map((it) => {
const entry = (it as any).webkitGetAsEntry?.()
if (entry) return traverseFileEntry(entry)
const f = it.getAsFile?.()
return f ? Promise.resolve([f]) : Promise.resolve([])
}),
)
let files = nested.flat()
if (notSupportBatchUpload) files = files.slice(0, 1)
const valid = files.filter(isValid)
initialUpload(valid)
},
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry],
)
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()

View File

@ -17,6 +17,7 @@ type Props = {
const NoData: FC<Props> = ({
onConfig,
provider,
}) => {
const { t } = useTranslation()
@ -38,7 +39,7 @@ const NoData: FC<Props> = ({
} : null,
}
const currentProvider = Object.values(providerConfig).find(provider => provider !== null) || providerConfig[DataSourceProvider.jinaReader]
const currentProvider = providerConfig[provider] || providerConfig[DataSourceProvider.jinaReader]
if (!currentProvider) return null

View File

@ -87,7 +87,7 @@ const Doc = ({ appDetail }: IDocProps) => {
<div className={`fixed right-8 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded
? (
<nav className="toc w-full rounded-lg bg-components-panel-bg p-4 shadow-md">
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button

View File

@ -643,13 +643,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -683,10 +681,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -699,13 +697,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -763,10 +759,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='action' type='string' key='action'>
动作,只能是 'enable' 或 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

View File

@ -845,6 +845,106 @@ Chat applications support session persistence, allowing previous chat history to
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='Get Conversation Variables'
name='#conversation-variables'
/>
<Row>
<Col>
Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation.
### Path Parameters
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
The ID of the conversation to retrieve variables from.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the application
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional) The ID of the last record on the current page, default is null.
</Property>
<Property name='limit' type='int' key='limit'>
(Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1.
</Property>
</Properties>
### Response
- `limit` (int) Number of items per page
- `has_more` (bool) Whether there is a next page
- `data` (array[object]) List of variables
- `id` (string) Variable ID
- `name` (string) Variable name
- `value_type` (string) Variable type (string, number, object, etc.)
- `value` (string) Variable value
- `description` (string) Variable description
- `created_at` (int) Creation timestamp
- `updated_at` (int) Last update timestamp
### Errors
- 404, `conversation_not_exists`, Conversation not found
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "Customer name extracted from the conversation",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "Order details from the customer",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'
@ -1237,13 +1337,11 @@ Chat applications support session persistence, allowing previous chat history to
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1277,10 +1375,10 @@ Chat applications support session persistence, allowing previous chat history to
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.apiBaseUrl}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1293,13 +1391,11 @@ Chat applications support session persistence, allowing previous chat history to
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1357,10 +1453,10 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='action' type='string' key='action'>
Action, can only be 'enable' or 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
Specified embedding model provider, must be set up in the system first, corresponding to the provider field(Optional)
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
Specified embedding model, corresponding to the model field(Optional)
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

View File

@ -844,6 +844,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='会話変数の取得'
name='#conversation-variables'
/>
<Row>
<Col>
特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。
### パスパラメータ
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
変数を取得する会話のID。
</Property>
</Properties>
### クエリパラメータ
<Properties>
<Property name='user' type='string' key='user'>
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional)現在のページの最後の記録のID、デフォルトはnullです。
</Property>
<Property name='limit' type='int' key='limit'>
(Optional)1回のリクエストで返す記録の数、デフォルトは最新の20件です。最大100、最小1。
</Property>
</Properties>
### レスポンス
- `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト
- `id` (string) 変数ID
- `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値
- `description` (string) 変数の説明
- `created_at` (int) 作成タイムスタンプ
- `updated_at` (int) 最終更新タイムスタンプ
### エラー
- 404, `conversation_not_exists`, 会話が見つかりません
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "会話から抽出された顧客名",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "顧客の注文詳細",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

View File

@ -881,6 +881,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='获取对话变量'
name='#conversation-variables'
/>
<Row>
<Col>
从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。
### 路径参数
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
要从中检索变量的对话ID。
</Property>
</Properties>
### 查询参数
<Properties>
<Property name='user' type='string' key='user'>
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
</Property>
<Property name='last_id' type='string' key='last_id'>
(选填)当前页最后面一条记录的 ID默认 null
</Property>
<Property name='limit' type='int' key='limit'>
(选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。
</Property>
</Properties>
### 响应
- `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表
- `id` (string) 变量ID
- `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值
- `description` (string) 变量描述
- `created_at` (int) 创建时间戳
- `updated_at` (int) 最后更新时间戳
### 错误
- 404, `conversation_not_exists`, 对话不存在
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "客户名称(从对话中提取)",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "客户的订单详情",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'
@ -1261,13 +1361,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1301,10 +1399,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
title="Request"
tag="PUT"
label="/apps/annotations/{annotation_id}"
targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
targetCode={`curl --location --request PUT '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"question": "What is your name?","answer": "I am Dify."}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \
curl --location --request PUT '${props.appDetail.api_base_url}/apps/annotations/{annotation_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
@ -1317,13 +1415,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
{
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
"id": "69d48372-ad81-4c75-9c46-2ce197b4d402",
"question": "What is your name?",
"answer": "I am Dify.",
"hit_count": 0,
"created_at": 1735625869
}
```
</CodeGroup>
@ -1381,10 +1477,10 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='action' type='string' key='action'>
动作,只能是 'enable' 或 'disable'
</Property>
<Property name='embedding_model_provider' type='string' key='embedding_model_provider'>
<Property name='embedding_provider_name' type='string' key='embedding_provider_name'>
指定的嵌入模型提供商, 必须先在系统内设定好接入的模型对应的是provider字段
</Property>
<Property name='embedding_model' type='string' key='embedding_model'>
<Property name='embedding_model_name' type='string' key='embedding_model_name'>
指定的嵌入模型对应的是model字段
</Property>
<Property name='score_threshold' type='number' key='score_threshold'>

View File

@ -878,6 +878,106 @@ Chat applications support session persistence, allowing previous chat history to
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='Get Conversation Variables'
name='#conversation-variables'
/>
<Row>
<Col>
Retrieve variables from a specific conversation. This endpoint is useful for extracting structured data that was captured during the conversation.
### Path Parameters
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
The ID of the conversation to retrieve variables from.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the application
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional) The ID of the last record on the current page, default is null.
</Property>
<Property name='limit' type='int' key='limit'>
(Optional) How many records to return in one request, default is the most recent 20 entries. Maximum 100, minimum 1.
</Property>
</Properties>
### Response
- `limit` (int) Number of items per page
- `has_more` (bool) Whether there is a next page
- `data` (array[object]) List of variables
- `id` (string) Variable ID
- `name` (string) Variable name
- `value_type` (string) Variable type (string, number, object, etc.)
- `value` (string) Variable value
- `description` (string) Variable description
- `created_at` (int) Creation timestamp
- `updated_at` (int) Last update timestamp
### Errors
- 404, `conversation_not_exists`, Conversation not found
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "Customer name extracted from the conversation",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "Order details from the customer",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

View File

@ -876,6 +876,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='会話変数の取得'
name='#conversation-variables'
/>
<Row>
<Col>
特定の会話から変数を取得します。このエンドポイントは、会話中に取得された構造化データを抽出するのに役立ちます。
### パスパラメータ
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
変数を取得する会話のID。
</Property>
</Properties>
### クエリパラメータ
<Properties>
<Property name='user' type='string' key='user'>
ユーザー識別子。開発者によって定義されたルールに従い、アプリケーション内で一意である必要があります。
</Property>
<Property name='last_id' type='string' key='last_id'>
(Optional)現在のページの最後のレコードのID、デフォルトはnullです。
</Property>
<Property name='limit' type='int' key='limit'>
(Optional)1回のリクエストで返すレコードの数、デフォルトは最新の20件です。最大100、最小1。
</Property>
</Properties>
### レスポンス
- `limit` (int) ページごとのアイテム数
- `has_more` (bool) さらにアイテムがあるかどうか
- `data` (array[object]) 変数のリスト
- `id` (string) 変数ID
- `name` (string) 変数名
- `value_type` (string) 変数タイプ(文字列、数値、真偽値など)
- `value` (string) 変数値
- `description` (string) 変数の説明
- `created_at` (int) 作成タイムスタンプ
- `updated_at` (int) 最終更新タイムスタンプ
### エラー
- 404, `conversation_not_exists`, 会話が見つかりません
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "会話から抽出された顧客名",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "顧客の注文詳細",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

View File

@ -893,6 +893,106 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
---
<Heading
url='/conversations/:conversation_id/variables'
method='GET'
title='获取对话变量'
name='#conversation-variables'
/>
<Row>
<Col>
从特定对话中检索变量。此端点对于提取对话过程中捕获的结构化数据非常有用。
### 路径参数
<Properties>
<Property name='conversation_id' type='string' key='conversation_id'>
要从中检索变量的对话ID。
</Property>
</Properties>
### 查询参数
<Properties>
<Property name='user' type='string' key='user'>
用户标识符,由开发人员定义的规则,在应用程序内必须唯一。
</Property>
<Property name='last_id' type='string' key='last_id'>
(选填)当前页最后面一条记录的 ID默认 null
</Property>
<Property name='limit' type='int' key='limit'>
(选填)一次请求返回多少条记录,默认 20 条,最大 100 条,最小 1 条。
</Property>
</Properties>
### 响应
- `limit` (int) 每页项目数
- `has_more` (bool) 是否有更多项目
- `data` (array[object]) 变量列表
- `id` (string) 变量ID
- `name` (string) 变量名称
- `value_type` (string) 变量类型(字符串、数字、布尔等)
- `value` (string) 变量值
- `description` (string) 变量描述
- `created_at` (int) 创建时间戳
- `updated_at` (int) 最后更新时间戳
### 错误
- 404, `conversation_not_exists`, 对话不存在
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations/:conversation_id/variables" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \\\n--header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Request with variable name filter">
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations/{conversation_id}/variables?user=abc-123&variable_name=customer_name' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"limit": 100,
"has_more": false,
"data": [
{
"id": "variable-uuid-1",
"name": "customer_name",
"value_type": "string",
"value": "John Doe",
"description": "客户名称(从对话中提取)",
"created_at": 1650000000000,
"updated_at": 1650000000000
},
{
"id": "variable-uuid-2",
"name": "order_details",
"value_type": "json",
"value": "{\"product\":\"Widget\",\"quantity\":5,\"price\":19.99}",
"description": "客户的订单详情",
"created_at": 1650000000000,
"updated_at": 1650000000000
}
]
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
url='/audio-to-text'
method='POST'

View File

@ -1,12 +1,10 @@
'use client'
import React, { useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import useSWR from 'swr'
import { useDebounceFn } from 'ahooks'
import Toast from '../../base/toast'
import s from './style.module.css'
import cn from '@/utils/classnames'
import ExploreContext from '@/context/explore-context'
@ -14,17 +12,16 @@ import type { App } from '@/models/explore'
import Category from '@/app/components/explore/category'
import AppCard from '@/app/components/explore/app-card'
import { fetchAppDetail, fetchAppList } from '@/service/explore'
import { importDSL } from '@/service/apps'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import Loading from '@/app/components/base/loading'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { getRedirection } from '@/utils/app-redirection'
import Input from '@/app/components/base/input'
import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import {
DSLImportMode,
} from '@/models/app'
import { useImportDSL } from '@/hooks/use-import-dsl'
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
type AppsProps = {
onSuccess?: () => void
@ -39,8 +36,6 @@ const Apps = ({
onSuccess,
}: AppsProps) => {
const { t } = useTranslation()
const { isCurrentWorkspaceEditor } = useAppContext()
const { push } = useRouter()
const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
@ -115,7 +110,14 @@ const Apps = ({
const [currApp, setCurrApp] = React.useState<App | null>(null)
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
const { handleCheckPluginDependencies } = usePluginDependencies()
const {
handleImportDSL,
handleImportDSLConfirm,
versions,
isFetching,
} = useImportDSL()
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
const onCreate: CreateAppModalProps['onConfirm'] = async ({
name,
icon_type,
@ -123,36 +125,34 @@ const Apps = ({
icon_background,
description,
}) => {
const { export_data, mode } = await fetchAppDetail(
const { export_data } = await fetchAppDetail(
currApp?.app.id as string,
)
try {
const app = await importDSL({
mode: DSLImportMode.YAML_CONTENT,
yaml_content: export_data,
name,
icon_type,
icon,
icon_background,
description,
})
setIsShowCreateModal(false)
Toast.notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
if (onSuccess)
onSuccess()
if (app.app_id)
await handleCheckPluginDependencies(app.app_id)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push)
}
catch {
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
const payload = {
mode: DSLImportMode.YAML_CONTENT,
yaml_content: export_data,
name,
icon_type,
icon,
icon_background,
description,
}
await handleImportDSL(payload, {
onSuccess: () => {
setIsShowCreateModal(false)
},
onPending: () => {
setShowDSLConfirmModal(true)
},
})
}
const onConfirmDSL = useCallback(async () => {
await handleImportDSLConfirm({
onSuccess,
})
}, [handleImportDSLConfirm, onSuccess])
if (!categories || categories.length === 0) {
return (
<div className="flex h-full items-center">
@ -225,9 +225,20 @@ const Apps = ({
appDescription={currApp?.app.description || ''}
show={isShowCreateModal}
onConfirm={onCreate}
confirmDisabled={isFetching}
onHide={() => setIsShowCreateModal(false)}
/>
)}
{
showDSLConfirmModal && (
<DSLConfirmModal
versions={versions}
onCancel={() => setShowDSLConfirmModal(false)}
onConfirm={onConfirmDSL}
confirmDisabled={isFetching}
/>
)
}
</div>
)
}

View File

@ -35,6 +35,7 @@ export type CreateAppModalProps = {
description: string
use_icon_as_answer_icon?: boolean
}) => Promise<void>
confirmDisabled?: boolean
onHide: () => void
}
@ -50,6 +51,7 @@ const CreateAppModal = ({
appMode,
appUseIconAsAnswerIcon,
onConfirm,
confirmDisabled,
onHide,
}: CreateAppModalProps) => {
const { t } = useTranslation()
@ -160,7 +162,7 @@ const CreateAppModal = ({
</div>
<div className='flex flex-row-reverse'>
<Button
disabled={(!isEditModal && isAppsFull) || !name.trim()}
disabled={(!isEditModal && isAppsFull) || !name.trim() || confirmDisabled}
className='ml-2 w-24 gap-1'
variant='primary'
onClick={handleSubmit}

View File

@ -3,7 +3,6 @@ import type {
Model,
ModelProvider,
} from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
@ -31,7 +30,7 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.icon_small) {
return (
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
<img alt='model-icon' src={basePath + renderI18nObject(provider.icon_small, language)}/>
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)}/>
</div>
)
}

View File

@ -39,6 +39,7 @@ const Input: FC<InputProps> = ({
return (
<div className='relative'>
<input
autoComplete="new-password"
tabIndex={0}
className={`
block h-8 w-full appearance-none rounded-lg border border-transparent bg-components-input-bg-normal px-3 text-sm

View File

@ -14,6 +14,7 @@ import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { fetchDatasetDetail, fetchDatasets } from '@/service/datasets'
import type { DataSetListResponse } from '@/models/datasets'
import { basePath } from '@/utils/var'
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more)
@ -56,7 +57,7 @@ const DatasetNav = () => {
icon_background: dataset.icon_background,
})) as NavItem[]}
createText={t('common.menus.newDataset')}
onCreate={() => router.push('/datasets/create')}
onCreate={() => router.push(`${basePath}/datasets/create`)}
onLoadmore={handleLoadmore}
/>
)

View File

@ -49,7 +49,7 @@ const Header = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment])
return (
<div className='flex flex-1 items-center justify-between bg-background-body px-4'>
<div className='relative flex flex-1 items-center justify-between bg-background-body'>
<div className='flex items-center'>
{isMobile && <div
className='flex h-8 w-8 cursor-pointer items-center justify-center'
@ -59,7 +59,7 @@ const Header = () => {
</div>}
{
!isMobile
&& <div className='flex w-64 shrink-0 items-center gap-1.5 self-stretch p-2 pl-3'>
&& <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'>
<Link href="/apps" className='flex h-8 w-8 shrink-0 items-center justify-center gap-2'>
<LogoSite className='object-contain' />
</Link>
@ -84,7 +84,7 @@ const Header = () => {
)}
{
!isMobile && (
<div className='flex items-center'>
<div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
@ -92,7 +92,7 @@ const Header = () => {
</div>
)
}
<div className='flex shrink-0 items-center'>
<div className='flex shrink-0 items-center pr-3'>
<EnvNav />
<div className='mr-2'>
<PluginsNav />

View File

@ -6,7 +6,7 @@ import {
RiArrowDownSLine,
RiArrowRightSLine,
} from '@remixicon/react'
import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation'
import { debounce } from 'lodash-es'
import cn from '@/utils/classnames'
@ -77,7 +77,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>
{
navs.map(nav => (
<MenuItems key={nav.id}>
<MenuItem key={nav.id}>
<div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-gray-700 hover:bg-gray-100' onClick={() => {
if (curNav?.id === nav.id)
return
@ -112,12 +112,12 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
{nav.name}
</div>
</div>
</MenuItems>
</MenuItem>
))
}
</div>
{!isApp && isCurrentWorkspaceEditor && (
<MenuButton className='w-full p-1'>
<MenuItem as="div" className='w-full p-1'>
<div onClick={() => onCreate('')} className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-gray-100',
)}>
@ -126,7 +126,7 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }:
</div>
<div className='grow text-left text-[14px] font-normal text-gray-700'>{createText}</div>
</div>
</MenuButton>
</MenuItem>
)}
{isApp && isCurrentWorkspaceEditor && (
<Menu as="div" className="relative h-full w-full">

View File

@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
const i18nPrefix = 'plugin.installModal'
type Props = {
@ -74,6 +75,7 @@ const Install: FC<Props> = ({
installedInfo: installedInfo!,
})
}
const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace()
return (
<>
<div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'>
@ -101,7 +103,7 @@ const Install: FC<Props> = ({
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0}
disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}

View File

@ -144,10 +144,18 @@ const PluginPage = ({
return activeTab === PLUGIN_PAGE_TABS_MAP.marketplace || values.includes(activeTab)
}, [activeTab])
const handleFileChange = (file: File | null) => {
if (!file || !file.name.endsWith('.difypkg')) {
setCurrentFile(null)
return
}
setCurrentFile(file)
}
const uploaderProps = useUploader({
onFileChange: setCurrentFile,
onFileChange: handleFileChange,
containerRef,
enabled: isPluginsTab,
enabled: isPluginsTab && canManagement,
})
const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps

View File

@ -3,6 +3,8 @@ import { useAppContext } from '@/context/app-context'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useMemo } from 'react'
const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
if (!permission)
@ -43,4 +45,17 @@ const usePermission = () => {
}
}
export const useCanInstallPluginFromMarketplace = () => {
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { canManagement } = usePermission()
const canInstallPluginFromMarketplace = useMemo(() => {
return enable_marketplace && canManagement
}, [enable_marketplace, canManagement])
return {
canInstallPluginFromMarketplace,
}
}
export default usePermission

View File

@ -3,9 +3,15 @@ import ChatVariableButton from '@/app/components/workflow/header/chat-variable-b
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { useIsChatMode } from '../../hooks'
const ChatVariableTrigger = () => {
const { nodesReadOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
if (!isChatMode)
return null
return <ChatVariableButton disabled={nodesReadOnly} />
}
export default memo(ChatVariableTrigger)

View File

@ -74,7 +74,7 @@ const WorkflowPanelOnRight = () => {
)
}
{
showChatVariablePanel && (
showChatVariablePanel && isChatMode && (
<ChatVariablePanel />
)
}

View File

@ -316,6 +316,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
nodesConnectable={!nodesReadOnly}
nodesFocusable={!nodesReadOnly}
edgesFocusable={!nodesReadOnly}
panOnScroll
panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
zoomOnPinch={!workflowReadOnly}
zoomOnScroll={!workflowReadOnly}

View File

@ -17,6 +17,7 @@ type Props = {
children?: React.JSX.Element | string | null
operations?: React.JSX.Element
inline?: boolean
required?: boolean
}
const Field: FC<Props> = ({
@ -28,6 +29,7 @@ const Field: FC<Props> = ({
operations,
inline,
supportFold,
required,
}) => {
const [fold, {
toggle: toggleFold,
@ -38,7 +40,9 @@ const Field: FC<Props> = ({
onClick={() => supportFold && toggleFold()}
className={cn('flex items-center justify-between', supportFold && 'cursor-pointer')}>
<div className='flex h-6 items-center'>
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>{title}</div>
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>
{title} {required && <span className='text-text-destructive'>*</span>}
</div>
{tooltip && (
<Tooltip
popupContent={tooltip}

View File

@ -596,17 +596,16 @@ const getIterationItemType = ({
arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type
}
else {
(valueSelector).slice(1).forEach((key, i) => {
for (let i = 1; i < valueSelector.length - 1; i++) {
const key = valueSelector[i]
const isLast = i === valueSelector.length - 2
curr = curr?.find((v: any) => v.variable === key)
if (isLast) {
arrayType = curr?.type
}
else {
if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
})
curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : []
if (isLast)
arrayType = curr?.type
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children || []
}
}
switch (arrayType as VarType) {
@ -631,7 +630,7 @@ const getLoopItemType = ({
}: {
valueSelector: ValueSelector
beforeNodesOutputVars: NodeOutPutVar[]
// eslint-disable-next-line sonarjs/no-identical-functions
}): VarType => {
const outputVarNodeId = valueSelector[0]
const isSystem = isSystemVar(valueSelector)

View File

@ -81,7 +81,11 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => {
const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey)
return <div className='my-2'>
<Field title={t('workflow.nodes.agent.strategy.label')} className='px-4 py-2' tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<Field
required
title={t('workflow.nodes.agent.strategy.label')}
className='px-4 py-2'
tooltip={t('workflow.nodes.agent.strategy.tooltip')} >
<AgentStrategy
strategy={inputs.agent_strategy_name ? {
agent_strategy_provider_name: inputs.agent_strategy_provider_name!,

View File

@ -117,8 +117,8 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
operations={
<AddButton onClick={handleAddOutputVariable} />
}
required
>
<OutputVarList
readonly={readOnly}
outputs={inputs.outputs}

View File

@ -64,6 +64,7 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<>
<VarReferencePicker

View File

@ -69,6 +69,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.api`)}
required
operations={
<div className='flex'>
<div
@ -126,6 +127,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.body`)}
required
>
<EditBody
nodeId={id}

View File

@ -73,6 +73,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.input`)}
required
operations={(
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
)}
@ -91,6 +92,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
<div className='mt-2 space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.output`)}
required
operations={(
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
)}

View File

@ -81,6 +81,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
{/* {JSON.stringify(inputs, null, 2)} */}
<Field
title={t(`${i18nPrefix}.queryVariable`)}
required
>
<VarReferencePicker
nodeId={id}
@ -94,6 +95,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
<Field
title={t(`${i18nPrefix}.knowledge`)}
required
operations={
<div className='flex items-center space-x-1'>
<RetrievalConfig

View File

@ -46,6 +46,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<VarReferencePicker
readonly={readOnly}

View File

@ -147,6 +147,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
<div className='space-y-4 px-4 pb-4'>
<Field
title={t(`${i18nPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'
@ -295,7 +296,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
onCollapse={setStructuredOutputCollapsed}
operations={
<div className='mr-4 flex shrink-0 items-center'>
{!isModelSupportStructuredOutput && (
{(!isModelSupportStructuredOutput && !!inputs.structured_output_enabled) && (
<Tooltip noDecoration popupContent={
<div className='w-[232px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg backdrop-blur-[5px]'>
<div className='title-xs-semi-bold text-text-primary'>{t('app.structOutput.modelNotSupported')}</div>

View File

@ -115,6 +115,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nCommonPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'
@ -133,6 +134,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.inputVar`)}
required
>
<>
<VarReferencePicker
@ -157,6 +159,7 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
/>
<Field
title={t(`${i18nPrefix}.extractParameters`)}
required
operations={
!readOnly
? (

View File

@ -63,7 +63,7 @@ const ClassList: FC<Props> = ({
return (
<Item
nodeId={nodeId}
key={index}
key={list[index].id}
payload={item}
onChange={handleClassChange(index)}
onRemove={handleRemoveClass(index)}

View File

@ -103,6 +103,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
<div className='space-y-4 px-4'>
<Field
title={t(`${i18nPrefix}.model`)}
required
>
<ModelParameterModal
popupClassName='!w-[387px]'
@ -121,6 +122,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
</Field>
<Field
title={t(`${i18nPrefix}.inputVars`)}
required
>
<VarReferencePicker
readonly={readOnly}
@ -143,6 +145,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
/>
<Field
title={t(`${i18nPrefix}.class`)}
required
>
<ClassList
nodeId={id}

View File

@ -47,8 +47,14 @@ const VariableModal = ({
return
if (!value)
return notify({ type: 'error', message: 'value can not be empty' })
if (!env && envList.some(env => env.name === name))
// Add check for duplicate name when editing
if (env && env.name !== name && envList.some(e => e.name === name))
return notify({ type: 'error', message: 'name is existed' })
// Original check for create new variable
if (!env && envList.some(e => e.name === name))
return notify({ type: 'error', message: 'name is existed' })
onSave({
id: env ? env.id : uuid4(),
value_type: type,

View File

@ -16,6 +16,7 @@ import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import { basePath } from '@/utils/var'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@ -80,12 +81,12 @@ const InstallForm = () => {
fetchSetupStatus().then((res: SetupStatusResponse) => {
if (res.step === 'finished') {
localStorage.setItem('setup_status', 'finished')
router.push('/signin')
router.push(`${basePath}/signin`)
}
else {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
if (res.status === 'not_started')
router.push('/init')
router.push(`${basePath}/init`)
})
}
setLoading(false)

View File

@ -54,6 +54,9 @@ const LocaleLayout = async ({
data-public-indexing-max-segmentation-tokens-length={process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
data-public-loop-node-max-count={process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT}
data-public-max-iterations-num={process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM}
data-public-enable-website-jinareader={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER}
data-public-enable-website-firecrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL}
data-public-enable-website-watercrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL}
>
<BrowserInitor>
<SentryInitor>