feat: update chat app publishing

This commit is contained in:
nite-knite 2024-04-03 13:07:57 +08:00
parent aca395b97d
commit 52a1c4580c
7 changed files with 144 additions and 85 deletions

View File

@ -5,55 +5,68 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import classNames from 'classnames'
import type { ModelAndParameter } from '../configuration/debug/types'
import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useGetLanguage } from '@/context/i18n'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { PlayCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
import { LeftIndent02 } from '@/app/components/base/icons/src/vender/line/editor'
import { FileText } from '@/app/components/base/icons/src/vender/line/files'
import { useGetLanguage } from '@/context/i18n'
export type AppPublisherProps = {
disabled?: boolean
publishDisabled?: boolean
publishedAt?: number
/** only needed in workflow / chatflow mode */
draftUpdatedAt?: number
onPublish?: () => Promise<void> | void
onRestore?: () => Promise<void> | void
debugWithMultipleModel?: boolean
multipleModelConfigs?: ModelAndParameter[]
/** modelAndParameter is passed when debugWithMultipleModel is true */
onPublish?: (modelAndParameter?: ModelAndParameter) => Promise<any> | any
onRestore?: () => Promise<any> | any
onToggle?: (state: boolean) => void
crossAxisOffset?: number
}
const AppPublisher = ({
disabled = false,
publishDisabled = false,
publishedAt,
draftUpdatedAt,
debugWithMultipleModel = false,
multipleModelConfigs = [],
onPublish,
onRestore,
onToggle,
crossAxisOffset = 0,
}: AppPublisherProps) => {
const { t } = useTranslation()
const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false)
const appDetail = useAppStore(state => state.appDetail)
const { app_base_url: appBaseURL, access_token } = appDetail?.site ?? {}
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}/${appMode}/${access_token}`
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
const language = useGetLanguage()
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
}, [language])
const handlePublish = async () => {
const handlePublish = async (modelAndParameter?: ModelAndParameter) => {
try {
await onPublish?.()
await onPublish?.(modelAndParameter)
setPublished(true)
}
catch (e) {
@ -70,22 +83,22 @@ const AppPublisher = ({
}, [onRestore])
const handleTrigger = useCallback(() => {
const state = !open
if (disabled) {
setOpen(false)
return
}
onToggle?.(!open)
onToggle?.(state)
setOpen(state)
if (open) {
setOpen(false)
}
else {
setOpen(true)
if (state)
setPublished(false)
}
}, [disabled, onToggle, open])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
@ -93,7 +106,7 @@ const AppPublisher = ({
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: -5,
crossAxis: crossAxisOffset,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
@ -137,35 +150,68 @@ const AppPublisher = ({
{t('workflow.common.autoSaved')} · {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
</div>
)}
<Button
type='primary'
className={`
mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium
${published && 'border-transparent'}
`}
onClick={handlePublish}
disabled={published}
>
{
published
? t('workflow.common.published')
: publishedAt ? t('workflow.common.update') : t('workflow.common.publish')
}
</Button>
{debugWithMultipleModel
? (
<PublishWithMultipleModel
multipleModelConfigs={multipleModelConfigs}
onSelect={item => handlePublish(item)}
// textGenerationModelList={textGenerationModelList}
/>
)
: (
<Button
type='primary'
className={classNames(
'mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium',
(publishDisabled || published) && 'border-transparent',
)}
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{
published
? t('workflow.common.published')
: publishedAt ? t('workflow.common.update') : t('workflow.common.publish')
}
</Button>
)
}
</div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
{appMode === 'chat'
{appDetail?.mode === 'workflow'
? (
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<CodeBrowser className='w-4 h-4' />}>{t('workflow.common.embedIntoSite')}</SuggestedAction>
<SuggestedAction
disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<LeftIndent02 className='w-4 h-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
)
: (
<SuggestedAction disabled={!publishedAt} link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`} icon={<LeftIndent02 className='w-4 h-4' />}>{t('workflow.common.batchRunApp')}</SuggestedAction>
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='w-4 h-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
</div>
</div>
</PortalToFollowElemContent>
<EmbeddedModal
isShow={embeddingModalOpen}
onClose={() => setEmbeddingModalOpen(false)}
appBaseUrl={appBaseURL}
accessToken={accessToken}
className='z-50'
/>
</PortalToFollowElem >
)
}

View File

@ -1,7 +1,8 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { ModelAndParameter } from '../types'
import type { ModelAndParameter } from '../configuration/debug/types'
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'
import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
@ -10,15 +11,17 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { useProviderContext } from '@/context/provider-context'
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
type PublishWithMultipleModelProps = {
multipleModelConfigs: ModelAndParameter[]
// textGenerationModelList?: Model[]
onSelect: (v: ModelAndParameter) => void
}
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
multipleModelConfigs,
// textGenerationModelList = [],
onSelect,
}) => {
const { t } = useTranslation()
@ -26,7 +29,7 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
const { textGenerationModelList } = useProviderContext()
const [open, setOpen] = useState(false)
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem })[] = []
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem; providerItem: Model })[] = []
multipleModelConfigs.forEach((item) => {
const provider = textGenerationModelList.find(model => model.provider === item.provider)
@ -40,6 +43,7 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
model: item.model,
provider: item.provider,
modelItem: model,
providerItem: provider,
parameters: item.parameters,
})
}
@ -62,18 +66,18 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
onOpenChange={setOpen}
placement='bottom-end'
>
<PortalToFollowElemTrigger onClick={handleToggle}>
<PortalToFollowElemTrigger className='w-full' onClick={handleToggle}>
<Button
type='primary'
disabled={!validModelConfigs.length}
className='pl-3 pr-2 h-8 text-[13px]'
className='mt-3 px-3 py-0 w-full h-8 border-[0.5px] border-primary-700 rounded-lg text-[13px] font-medium'
>
{t('appDebug.operation.applyConfig')}
<ChevronDown className='ml-0.5 w-3 h-3' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='p-1 w-[168px] rounded-lg border-[0.5px] border-gray-200 shadow-lg bg-white'>
<PortalToFollowElemContent className='mt-1 w-[288px] z-50'>
<div className='p-1 rounded-lg border-[0.5px] border-gray-200 shadow-lg bg-white'>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
{t('appDebug.publishAs')}
</div>
@ -81,10 +85,11 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
validModelConfigs.map((item, index) => (
<div
key={item.id}
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-100 cursor-pointer text-sm text-gray-500'
className='flex items-center h-8 px-3 text-sm text-gray-500 rounded-lg cursor-pointer hover:bg-gray-100'
onClick={() => handleSelect(item)}
>
#{index + 1}
<span className='italic min-w-[18px]'>#{index + 1}</span>
<ModelIcon modelName={item.model} provider={item.providerItem} className='ml-2' />
<div
className='ml-1 text-gray-700 truncate'
title={item.modelItem.label[language]}

View File

@ -1,15 +1,25 @@
import type { PropsWithChildren } from 'react'
import type { HTMLProps, PropsWithChildren } from 'react'
import classNames from 'classnames'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
export type SuggestedActionProps = PropsWithChildren<{
export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
icon?: React.ReactNode
link?: string
disabled?: boolean
}>
const SuggestedAction = ({ icon, link, disabled, children }: SuggestedActionProps) => (
<a href={disabled ? undefined : link} target='_blank' rel='noreferrer' className={classNames('flex justify-start items-center gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1', disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer')}>
const SuggestedAction = ({ icon, link, disabled, children, className, ...props }: SuggestedActionProps) => (
<a
href={disabled ? undefined : link}
target='_blank'
rel='noreferrer'
className={classNames(
'flex justify-start items-center gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
className,
)}
{...props}
>
<div className='relative w-4 h-4'>{icon}</div>
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
<ArrowUpRight />

View File

@ -1,16 +1,16 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { usePathname } from 'next/navigation'
import produce from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import cn from 'classnames'
import { clone, isEqual } from 'lodash-es'
import { CodeBracketIcon } from '@heroicons/react/20/solid'
import Button from '../../base/button'
import Loading from '../../base/loading'
import AppPublisher from '../app-publisher'
import AgentSettingButton from './config/agent-setting-button'
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
@ -19,7 +19,6 @@ import {
useFormattingChangedDispatcher,
} from './debug/hooks'
import type { ModelAndParameter } from './debug/types'
import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model'
import type {
AnnotationReplyConfig,
DatasetConfigs,
@ -66,7 +65,7 @@ type PublichConfig = {
const Configuration: FC = () => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { setAppSiderbarExpand } = useAppStore()
const { appDetail, setAppSiderbarExpand } = useAppStore()
const [formattingChanged, setFormattingChanged] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
@ -77,6 +76,7 @@ const Configuration: FC = () => {
const [mode, setMode] = useState('')
const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null)
const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail])
const [conversationId, setConversationId] = useState<string | null>('')
const media = useBreakpoints()
@ -130,7 +130,7 @@ const Configuration: FC = () => {
const [inputs, setInputs] = useState<Inputs>({})
const [query, setQuery] = useState('')
const [completionParams, doSetCompletionParams] = useState<FormValue>({})
const [tempStop, setTempStop, getTempStop] = useGetState<string[]>([])
const [_, setTempStop, getTempStop] = useGetState<string[]>([])
const setCompletionParams = (value: FormValue) => {
const params = { ...value }
@ -508,6 +508,7 @@ const Configuration: FC = () => {
setHasFetchedDetail(true)
})
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId])
const promptEmpty = (() => {
@ -541,7 +542,7 @@ const Configuration: FC = () => {
else { return promptEmpty }
})()
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
const handlePublish = async (isSilence?: boolean, modelAndParameter?: ModelAndParameter) => {
const onPublish = async (modelAndParameter?: ModelAndParameter) => {
const modelId = modelAndParameter?.model || modelConfig.model_id
const promptTemplate = modelConfig.configs.prompt_template
const promptVariables = modelConfig.configs.prompt_variables
@ -630,17 +631,16 @@ const Configuration: FC = () => {
modelConfig: newModelConfig,
completionParams,
})
if (!isSilence)
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
setCanReturnToSimpleMode(false)
return true
}
const [showConfirm, setShowConfirm] = useState(false)
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const resetAppConfig = () => {
syncToPublishedConfig(publishedConfig!)
setShowConfirm(false)
setRestoreConfirmOpen(false)
}
const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
@ -788,26 +788,20 @@ const Configuration: FC = () => {
<div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
</>
)}
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium text-gray-900'>{t('appDebug.operation.resetConfig')}</Button>
{isMobile && (
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
<CodeBracketIcon className="w-4 h-4 text-gray-500" />
</Button>
)}
{debugWithMultipleModel
? (<PublishWithMultipleModel
multipleModelConfigs={multipleModelConfigs}
onSelect={item => handlePublish(false, item)}
/>)
: (<Button
type='primary'
onClick={() => handlePublish(false)}
className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}
>
{t('appDebug.operation.applyConfig')}
</Button>)}
{/* <Publish /> */}
<AppPublisher {...{
publishDisabled: cannotPublish,
publishedAt: (modalConfig.created_at || 0) * 1000,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
onRestore: () => setRestoreConfirmOpen(true),
}} />
</div>
</div>
</div>
@ -832,14 +826,14 @@ const Configuration: FC = () => {
</div>}
</div>
</div>
{showConfirm && (
{restoreConfirmOpen && (
<Confirm
title={t('appDebug.resetConfig.title')}
content={t('appDebug.resetConfig.message')}
isShow={showConfirm}
onClose={() => setShowConfirm(false)}
isShow={restoreConfirmOpen}
onClose={() => setRestoreConfirmOpen(false)}
onConfirm={resetAppConfig}
onCancel={() => setShowConfirm(false)}
onCancel={() => setRestoreConfirmOpen(false)}
/>
)}
{showUseGPT4Confirm && (

View File

@ -14,6 +14,7 @@ type Props = {
onClose: () => void
accessToken: string
appBaseUrl: string
className?: string
}
const OPTION_MAP = {
@ -22,19 +23,19 @@ const OPTION_MAP = {
`<iframe
src="${url}/chatbot/${token}"
style="width: 100%; height: 100%; min-height: 700px"
frameborder="0"
frameborder="0"
allow="microphone">
</iframe>`,
},
scripts: {
getContent: (url: string, token: string, isTestEnv?: boolean) =>
`<script>
window.difyChatbotConfig = {
window.difyChatbotConfig = {
token: '${token}'${isTestEnv
? `,
? `,
isDev: true`
: ''}${IS_CE_EDITION
? `,
? `,
baseUrl: '${url}'`
: ''}
}
@ -59,7 +60,7 @@ type OptionStatus = {
chromePlugin: boolean
}
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
const Embedded = ({ isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
const { t } = useTranslation()
const [option, setOption] = useState<Option>('iframe')
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
@ -101,12 +102,13 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
isShow={isShow}
onClose={onClose}
className="!max-w-2xl w-[640px]"
wrapperClassName={className}
closable={true}
>
<div className="mb-4 mt-8 text-gray-900 text-[14px] font-medium leading-tight">
{t(`${prefixEmbedded}.explanation`)}
</div>
<div className="flex items-center justify-between flex-wrap gap-y-2">
<div className="flex flex-wrap items-center justify-between gap-y-2">
{Object.keys(OPTION_MAP).map((v, index) => {
return (
<div
@ -125,7 +127,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
})}
</div>
{option === 'chromePlugin' && (
<div className="mt-6 w-full">
<div className="w-full mt-6">
<div className={cn('gap-2 py-3 justify-center items-center inline-flex w-full rounded-lg',
'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm flex-shrink-0')}>
<div className={`w-4 h-4 relative ${style.pluginInstallIcon}`}></div>
@ -135,22 +137,22 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
)}
<div className={cn('w-full bg-gray-100 rounded-lg flex-col justify-start items-start inline-flex',
'mt-6')}>
<div className="self-stretch pl-3 pr-1 py-1 bg-gray-50 rounded-tl-lg rounded-tr-lg border border-black border-opacity-5 justify-start items-center gap-2 inline-flex">
<div className="inline-flex items-center self-stretch justify-start gap-2 py-1 pl-3 pr-1 border border-black rounded-tl-lg rounded-tr-lg bg-gray-50 border-opacity-5">
<div className="grow shrink basis-0 text-slate-700 text-[13px] font-medium leading-none">
{t(`${prefixEmbedded}.${option}`)}
</div>
<div className="p-2 rounded-lg justify-center items-center gap-1 flex">
<div className="flex items-center justify-center gap-1 p-2 rounded-lg">
<Tooltip
selector={'code-copy-feedback'}
content={(isCopied[option] ? t(`${prefixEmbedded}.copied`) : t(`${prefixEmbedded}.copy`)) || ''}
>
<div className="w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg">
<div className="w-8 h-8 rounded-lg cursor-pointer hover:bg-gray-100">
<div onClick={onClickCopy} className={`w-full h-full ${copyStyle.copyIcon} ${isCopied[option] ? copyStyle.copied : ''}`}></div>
</div>
</Tooltip>
</div>
</div>
<div className="p-3 justify-start items-start gap-2 flex overflow-x-auto w-full">
<div className="flex items-start justify-start w-full gap-2 p-3 overflow-x-auto">
<div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono">
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)}</pre>
</div>

View File

@ -162,6 +162,7 @@ const Header: FC = () => {
onPublish,
onRestore: onStartRestoring,
onToggle: onPublisherToggle,
crossAxisOffset: 53,
}}
/>
{

View File

@ -228,6 +228,7 @@ export type ModelConfig = {
image: VisionSettings
}
files?: VisionFile[]
created_at?: number
}
export type Language = typeof LanguagesSupported[number]