chore: merge

This commit is contained in:
Joel 2025-01-09 13:55:37 +08:00
commit 371febf3cf
35 changed files with 320 additions and 266 deletions

View File

@ -82,6 +82,33 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: pnpm run lint run: pnpm run lint
docker-compose-template:
name: Docker Compose Template
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: |
docker/generate_docker_compose
docker/.env.example
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- name: Generate Docker Compose
if: steps.changed-files.outputs.any_changed == 'true'
run: |
cd docker
./generate_docker_compose
- name: Check for changes
if: steps.changed-files.outputs.any_changed == 'true'
run: git diff --exit-code
superlinter: superlinter:
name: SuperLinter name: SuperLinter

View File

@ -513,7 +513,7 @@ TENCENT_VECTOR_DB_SHARD=1
TENCENT_VECTOR_DB_REPLICAS=2 TENCENT_VECTOR_DB_REPLICAS=2
# ElasticSearch configuration, only available when VECTOR_STORE is `elasticsearch` # ElasticSearch configuration, only available when VECTOR_STORE is `elasticsearch`
ELASTICSEARCH_HOST=elasticsearch ELASTICSEARCH_HOST=0.0.0.0
ELASTICSEARCH_PORT=9200 ELASTICSEARCH_PORT=9200
ELASTICSEARCH_USERNAME=elastic ELASTICSEARCH_USERNAME=elastic
ELASTICSEARCH_PASSWORD=elastic ELASTICSEARCH_PASSWORD=elastic

View File

@ -451,7 +451,7 @@ services:
milvus-standalone: milvus-standalone:
container_name: milvus-standalone container_name: milvus-standalone
image: milvusdb/milvus:v2.3.1 image: milvusdb/milvus:v2.5.0-beta
profiles: profiles:
- milvus - milvus
command: [ 'milvus', 'run', 'standalone' ] command: [ 'milvus', 'run', 'standalone' ]
@ -535,20 +535,28 @@ services:
container_name: elasticsearch container_name: elasticsearch
profiles: profiles:
- elasticsearch - elasticsearch
- elasticsearch-ja
restart: always restart: always
volumes: volumes:
- ./elasticsearch/docker-entrypoint.sh:/docker-entrypoint-mount.sh
- dify_es01_data:/usr/share/elasticsearch/data - dify_es01_data:/usr/share/elasticsearch/data
environment: environment:
ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic} ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD:-elastic}
VECTOR_STORE: ${VECTOR_STORE:-}
cluster.name: dify-es-cluster cluster.name: dify-es-cluster
node.name: dify-es0 node.name: dify-es0
discovery.type: single-node discovery.type: single-node
xpack.license.self_generated.type: trial xpack.license.self_generated.type: basic
xpack.security.enabled: 'true' xpack.security.enabled: 'true'
xpack.security.enrollment.enabled: 'false' xpack.security.enrollment.enabled: 'false'
xpack.security.http.ssl.enabled: 'false' xpack.security.http.ssl.enabled: 'false'
ports: ports:
- ${ELASTICSEARCH_PORT:-9200}:9200 - ${ELASTICSEARCH_PORT:-9200}:9200
deploy:
resources:
limits:
memory: 2g
entrypoint: [ 'sh', '-c', "sh /docker-entrypoint-mount.sh" ]
healthcheck: healthcheck:
test: [ 'CMD', 'curl', '-s', 'http://localhost:9200/_cluster/health?pretty' ] test: [ 'CMD', 'curl', '-s', 'http://localhost:9200/_cluster/health?pretty' ]
interval: 30s interval: 30s

View File

@ -11,6 +11,8 @@ import ProviderConfigModal from './provider-config-modal'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Divider from '@/app/components/base/divider'
import cn from '@/utils/classnames'
const I18N_PREFIX = 'app.tracing' const I18N_PREFIX = 'app.tracing'
@ -77,7 +79,6 @@ const ConfigPopup: FC<PopupProps> = ({
className='ml-3' className='ml-3'
defaultValue={enabled} defaultValue={enabled}
onChange={onStatusChange} onChange={onStatusChange}
size='l'
disabled={providerAllNotConfigured} disabled={providerAllNotConfigured}
/> />
) )
@ -106,15 +107,15 @@ const ConfigPopup: FC<PopupProps> = ({
) )
return ( return (
<div className='w-[420px] p-4 rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg'> <div className='w-[420px] p-4 rounded-2xl bg-components-panel-bg border-[0.5px] border-components-panel-border shadow-xl'>
<div className='flex justify-between items-center'> <div className='flex justify-between items-center'>
<div className='flex items-center'> <div className='flex items-center'>
<TracingIcon size='md' className='mr-2' /> <TracingIcon size='md' className='mr-2' />
<div className='leading-[120%] text-[18px] font-semibold text-gray-900'>{t(`${I18N_PREFIX}.tracing`)}</div> <div className='text-text-primary title-2xl-semibold'>{t(`${I18N_PREFIX}.tracing`)}</div>
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
<Indicator color={enabled ? 'green' : 'gray'} /> <Indicator color={enabled ? 'green' : 'gray'} />
<div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'> <div className={cn('ml-1 system-xs-semibold-uppercase text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)} {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
</div> </div>
{!readOnly && ( {!readOnly && (
@ -130,19 +131,18 @@ const ConfigPopup: FC<PopupProps> = ({
: switchContent} : switchContent}
</> </>
)} )}
</div> </div>
</div> </div>
<div className='mt-2 leading-4 text-xs font-normal text-gray-500'> <div className='mt-2 system-xs-regular text-text-tertiary'>
{t(`${I18N_PREFIX}.tracingDescription`)} {t(`${I18N_PREFIX}.tracingDescription`)}
</div> </div>
<div className='mt-3 h-px bg-gray-100'></div> <Divider className='my-3' />
<div className='mt-3'> <div className='relative'>
{(providerAllConfigured || providerAllNotConfigured) {(providerAllConfigured || providerAllNotConfigured)
? ( ? (
<> <>
<div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div> <div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
<div className='mt-2 space-y-2'> <div className='mt-2 space-y-2'>
{langSmithPanel} {langSmithPanel}
{langfusePanel} {langfusePanel}
@ -151,11 +151,11 @@ const ConfigPopup: FC<PopupProps> = ({
) )
: ( : (
<> <>
<div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div> <div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
<div className='mt-2'> <div className='mt-2'>
{langSmithConfig ? langSmithPanel : langfusePanel} {langSmithConfig ? langSmithPanel : langfusePanel}
</div> </div>
<div className='mt-3 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div> <div className='mt-3 system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
<div className='mt-2'> <div className='mt-2'>
{!langSmithConfig ? langSmithPanel : langfusePanel} {!langSmithConfig ? langSmithPanel : langfusePanel}
</div> </div>

View File

@ -26,7 +26,7 @@ const Field: FC<Props> = ({
return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<div className='flex py-[7px]'> <div className='flex py-[7px]'>
<div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div> <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-text-primary')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>} {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
</div> </div>
<Input <Input

View File

@ -17,6 +17,7 @@ import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/gene
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import Divider from '@/app/components/base/divider'
type Props = { type Props = {
appId: string appId: string
@ -152,11 +153,11 @@ const ProviderConfigModal: FC<Props> = ({
? ( ? (
<PortalToFollowElem open> <PortalToFollowElem open>
<PortalToFollowElemContent className='w-full h-full z-[60]'> <PortalToFollowElemContent className='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'> <div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'> <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'> <div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-4'> <div className='flex justify-between items-center mb-4'>
<div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div> <div className='title-2xl-semibold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
</div> </div>
<div className='space-y-4'> <div className='space-y-4'>
@ -230,16 +231,16 @@ const ProviderConfigModal: FC<Props> = ({
{isEdit && ( {isEdit && (
<> <>
<Button <Button
className='h-9 text-sm font-medium text-gray-700' className='h-9 text-sm font-medium text-text-secondary'
onClick={showRemoveConfirm} onClick={showRemoveConfirm}
> >
<span className='text-[#D92D20]'>{t('common.operation.remove')}</span> <span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
</Button> </Button>
<div className='mx-3 w-px h-[18px] bg-gray-200'></div> <Divider className='mx-3 h-[18px]'/>
</> </>
)} )}
<Button <Button
className='mr-2 h-9 text-sm font-medium text-gray-700' className='mr-2 h-9 text-sm font-medium text-text-secondary'
onClick={onCancel} onClick={onCancel}
> >
{t('common.operation.cancel')} {t('common.operation.cancel')}
@ -256,9 +257,9 @@ const ProviderConfigModal: FC<Props> = ({
</div> </div>
</div> </div>
<div className='border-t-[0.5px] border-t-black/5'> <div className='border-t-[0.5px] border-divider-regular'>
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'> <div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' /> <Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
{t('common.modelProvider.encrypted.front')} {t('common.modelProvider.encrypted.front')}
<a <a
className='text-primary-600 mx-1' className='text-primary-600 mx-1'

View File

@ -1,11 +1,13 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TracingProvider } from './type' import { TracingProvider } from './type'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing' import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing'
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general'
const I18N_PREFIX = 'app.tracing' const I18N_PREFIX = 'app.tracing'
@ -61,34 +63,37 @@ const ProviderPanel: FC<Props> = ({
}, [hasConfigured, isChosen, onChoose, readOnly]) }, [hasConfigured, isChosen, onChoose, readOnly])
return ( return (
<div <div
className={cn(isChosen ? 'border-primary-400' : 'border-transparent', !isChosen && hasConfigured && !readOnly && 'cursor-pointer', 'px-4 py-3 rounded-xl border-[1.5px] bg-gray-100')} className={cn(
'px-4 py-3 rounded-xl border-[1.5px] bg-background-section-burn',
isChosen ? 'bg-background-section border-components-option-card-option-selected-border' : 'border-transparent',
!isChosen && hasConfigured && !readOnly && 'cursor-pointer',
)}
onClick={handleChosen} onClick={handleChosen}
> >
<div className={'flex justify-between items-center space-x-1'}> <div className={'flex justify-between items-center space-x-1'}>
<div className='flex items-center'> <div className='flex items-center'>
<Icon className='h-6' /> <Icon className='h-6' />
{isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>} {isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-text-accent-secondary system-2xs-medium-uppercase text-text-accent-secondary'>{t(`${I18N_PREFIX}.inUse`)}</div>}
</div> </div>
{!readOnly && ( {!readOnly && (
<div className={'flex justify-between items-center space-x-1'}> <div className={'flex justify-between items-center space-x-1'}>
{hasConfigured && ( {hasConfigured && (
<div className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' onClick={viewBtnClick} > <div className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1' onClick={viewBtnClick} >
<View className='w-3 h-3'/> <View className='w-3 h-3'/>
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div> <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
</div> </div>
)} )}
<div <div
className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1' className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1'
onClick={handleConfigBtnClick} onClick={handleConfigBtnClick}
> >
<Settings04 className='w-3 h-3' /> <RiEqualizer2Line className='w-3 h-3' />
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div> <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
</div> </div>
</div> </div>
)} )}
</div> </div>
<div className='mt-2 leading-4 text-xs font-normal text-gray-500'> <div className='mt-2 system-xs-regular text-text-tertiary'>
{t(`${I18N_PREFIX}.${type}.description`)} {t(`${I18N_PREFIX}.${type}.description`)}
</div> </div>
</div> </div>

View File

@ -27,8 +27,8 @@ const APIKeyInfoPanel: FC = () => {
return null return null
return ( return (
<div className={cn('bg-[#EFF4FF] border-[#D1E0FF]', 'mb-6 relative rounded-2xl shadow-md border p-8 ')}> <div className={cn('bg-components-panel-bg border-components-panel-border', 'mb-6 relative rounded-2xl shadow-md border p-8 ')}>
<div className={cn('text-[24px] text-gray-800 font-semibold', isCloud ? 'flex items-center h-8 space-x-1' : 'leading-8 mb-6')}> <div className={cn('text-[24px] text-text-primary font-semibold', isCloud ? 'flex items-center h-8 space-x-1' : 'leading-8 mb-6')}>
{isCloud && <em-emoji id={'😀'} />} {isCloud && <em-emoji id={'😀'} />}
{isCloud {isCloud
? ( ? (
@ -42,11 +42,11 @@ const APIKeyInfoPanel: FC = () => {
)} )}
</div> </div>
{isCloud && ( {isCloud && (
<div className='mt-1 text-sm text-gray-600 font-normal'>{t(`appOverview.apiKeyInfo.cloud.${'trial'}.description`)}</div> <div className='mt-1 text-sm text-text-tertiary font-normal'>{t(`appOverview.apiKeyInfo.cloud.${'trial'}.description`)}</div>
)} )}
<Button <Button
variant='primary' variant='primary'
className='space-x-2' className='mt-2 space-x-2'
onClick={() => setShowAccountSettingModal({ payload: 'provider' })} onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
> >
<div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div> <div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div>
@ -65,7 +65,7 @@ const APIKeyInfoPanel: FC = () => {
<div <div
onClick={() => setIsShow(false)} onClick={() => setIsShow(false)}
className='absolute right-4 top-4 flex items-center justify-center w-8 h-8 cursor-pointer '> className='absolute right-4 top-4 flex items-center justify-center w-8 h-8 cursor-pointer '>
<RiCloseLine className='w-4 h-4 text-gray-500' /> <RiCloseLine className='w-4 h-4 text-text-tertiary' />
</div> </div>
</div> </div>
) )

View File

@ -1,29 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
export type IProgressProps = {
className?: string
value: number // percent
}
const Progress: FC<IProgressProps> = ({
className,
value,
}) => {
const exhausted = value === 100
return (
<div className={cn(className, 'relative grow h-2 flex bg-gray-200 rounded-md overflow-hidden')}>
<div
className={cn(s.bar, exhausted && s['bar-error'], 'absolute top-0 left-0 right-0 bottom-0')}
style={{ width: `${value}%` }}
/>
{Array.from({ length: 10 }).fill(0).map((i, k) => (
<div key={k} className={s['bar-item']} />
))}
</div>
)
}
export default React.memo(Progress)

View File

@ -1,16 +0,0 @@
.bar {
background: linear-gradient(90deg, rgba(41, 112, 255, 0.9) 0%, rgba(21, 94, 239, 0.9) 100%);
}
.bar-error {
background: linear-gradient(90deg, rgba(240, 68, 56, 0.72) 0%, rgba(217, 45, 32, 0.9) 100%);
}
.bar-item {
width: 10%;
border-right: 1px solid rgba(255, 255, 255, 0.5);
}
.bar-item:last-of-type {
border-right: 0;
}

View File

@ -1,6 +1,9 @@
'use client' 'use client'
import type { HTMLProps } from 'react' import type { HTMLProps } from 'react'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import {
RiLoopLeftLine,
} from '@remixicon/react'
import { import {
Cog8ToothIcon, Cog8ToothIcon,
DocumentTextIcon, DocumentTextIcon,
@ -16,24 +19,25 @@ import style from './style.module.css'
import type { ConfigParams } from './settings' import type { ConfigParams } from './settings'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import AppBasic from '@/app/components/app-sidebar/basic' import AppBasic from '@/app/components/app-sidebar/basic'
import { asyncRunSafe, randomString } from '@/utils' import { asyncRunSafe } from '@/utils'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Tag from '@/app/components/base/tag' import Tag from '@/app/components/base/tag'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import CopyFeedback from '@/app/components/base/copy-feedback' import CopyFeedback from '@/app/components/base/copy-feedback'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import ShareQRCode from '@/app/components/base/qrcode' import ShareQRCode from '@/app/components/base/qrcode'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button' import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import type { AppDetailResponse } from '@/models/app' import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app' import type { AppSSO } from '@/types/app'
import cn from '@/utils/classnames'
export type IAppCardProps = { export type IAppCardProps = {
className?: string className?: string
appInfo: AppDetailResponse & Partial<AppSSO> appInfo: AppDetailResponse & Partial<AppSSO>
cardType?: 'api' | 'webapp' cardType?: 'api' | 'webapp'
customBgColor?: string
onChangeStatus: (val: boolean) => Promise<void> onChangeStatus: (val: boolean) => Promise<void>
onSaveSiteConfig?: (params: ConfigParams) => Promise<void> onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
onGenerateCode?: () => Promise<void> onGenerateCode?: () => Promise<void>
@ -46,7 +50,6 @@ const EmbedIcon = ({ className = '' }: HTMLProps<HTMLDivElement>) => {
function AppCard({ function AppCard({
appInfo, appInfo,
cardType = 'webapp', cardType = 'webapp',
customBgColor,
onChangeStatus, onChangeStatus,
onSaveSiteConfig, onSaveSiteConfig,
onGenerateCode, onGenerateCode,
@ -92,10 +95,6 @@ function AppCard({
const appUrl = `${app_base_url}/${appMode}/${access_token}` const appUrl = `${app_base_url}/${appMode}/${access_token}`
const apiUrl = appInfo?.api_base_url const apiUrl = appInfo?.api_base_url
let bgColor = 'bg-primary-50 bg-opacity-40'
if (cardType === 'api')
bgColor = 'bg-purple-50'
const genClickFuncByName = (opName: string) => { const genClickFuncByName = (opName: string) => {
switch (opName) { switch (opName) {
case t('appOverview.overview.appInfo.preview'): case t('appOverview.overview.appInfo.preview'):
@ -133,11 +132,8 @@ function AppCard({
} }
return ( return (
<div <div className={cn('rounded-xl border-effects-highlight border-t border-l-[0.5px] bg-background-default', className)}>
className={ <div className={cn('px-6 py-5')}>
`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
>
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between"> <div className="mb-2.5 flex flex-row items-start justify-between">
<AppBasic <AppBasic
iconType={cardType} iconType={cardType}
@ -161,23 +157,20 @@ function AppCard({
</div> </div>
<div className="flex flex-col justify-center py-2"> <div className="flex flex-col justify-center py-2">
<div className="py-1"> <div className="py-1">
<div className="pb-1 text-xs text-gray-500"> <div className="pb-1 text-xs text-text-tertiary">
{isApp {isApp
? t('appOverview.overview.appInfo.accessibleAddress') ? t('appOverview.overview.appInfo.accessibleAddress')
: t('appOverview.overview.apiInfo.accessibleAddress')} : t('appOverview.overview.apiInfo.accessibleAddress')}
</div> </div>
<div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-2 rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex"> <div className="w-full h-9 px-2 py-0.5 bg-components-input-bg-normal rounded-lg justify-start items-center inline-flex">
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0"> <div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0">
<div className="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap"> <div className="text-text-secondary system-xs-medium truncate">
{isApp ? appUrl : apiUrl} {isApp ? appUrl : apiUrl}
</div> </div>
</div> </div>
<Divider type="vertical" className="!h-3.5 shrink-0 !mx-0.5" /> <Divider type="vertical" className="!h-3.5 shrink-0" />
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} selectorId={randomString(8)} className={'hover:bg-gray-200'} />} {isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} />}
<CopyFeedback <CopyFeedback content={isApp ? appUrl : apiUrl}/>
content={isApp ? appUrl : apiUrl}
className={'hover:bg-gray-200'}
/>
{/* button copy link/ button regenerate */} {/* button copy link/ button regenerate */}
{showConfirmDelete && ( {showConfirmDelete && (
<Confirm <Confirm
@ -196,22 +189,16 @@ function AppCard({
<Tooltip <Tooltip
popupContent={t('appOverview.overview.appInfo.regenerate') || ''} popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
> >
<div <ActionButton onClick={() => setShowConfirmDelete(true)}>
className="w-8 h-8 ml-0.5 cursor-pointer hover:bg-gray-200 rounded-lg" <RiLoopLeftLine className={cn('w-4 h-4', genLoading && 'animate-spin')} />
onClick={() => setShowConfirmDelete(true)} </ActionButton>
>
<div
className={
`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
></div>
</div>
</Tooltip> </Tooltip>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className={'pt-2 flex flex-row items-center flex-wrap gap-y-2'}> <div className={'pt-2 flex flex-row items-center flex-wrap gap-y-2'}>
{!isApp && <SecretKeyButton className='flex-shrink-0 !h-8 bg-white mr-2' textCls='!text-gray-700 font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />} {!isApp && <SecretKeyButton className='shrink-0 !h-8 mr-2' textCls='!text-text-secondary font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
{OPERATIONS_MAP[cardType].map((op) => { {OPERATIONS_MAP[cardType].map((op) => {
const disabled const disabled
= op.opName === t('appOverview.overview.appInfo.settings.entry') = op.opName === t('appOverview.overview.appInfo.settings.entry')

View File

@ -1,10 +1,15 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiClipboardFill,
RiClipboardLine,
} from '@remixicon/react'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import copyStyle from './style.module.css' import copyStyle from './style.module.css'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import ActionButton from '@/app/components/base/action-button'
type Props = { type Props = {
content: string content: string
@ -13,7 +18,7 @@ type Props = {
const prefixEmbedded = 'appOverview.overview.appInfo.embedded' const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
const CopyFeedback = ({ content, className }: Props) => { const CopyFeedback = ({ content }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false) const [isCopied, setIsCopied] = useState<boolean>(false)
@ -34,19 +39,15 @@ const CopyFeedback = ({ content, className }: Props) => {
: t(`${prefixEmbedded}.copy`)) || '' : t(`${prefixEmbedded}.copy`)) || ''
} }
> >
<div <ActionButton>
className={`w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg ${
className ?? ''
}`}
>
<div <div
onClick={onClickCopy} onClick={onClickCopy}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
className={`w-full h-full ${copyStyle.copyIcon} ${ >
isCopied ? copyStyle.copied : '' {isCopied && <RiClipboardFill className='w-4 h-4' />}
}`} {!isCopied && <RiClipboardLine className='w-4 h-4' />}
></div> </div>
</div> </ActionButton>
</Tooltip> </Tooltip>
) )
} }

View File

@ -1,19 +1,20 @@
'use client' 'use client'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiQrCodeLine,
} from '@remixicon/react'
import { QRCodeCanvas as QRCode } from 'qrcode.react' import { QRCodeCanvas as QRCode } from 'qrcode.react'
import QrcodeStyle from './style.module.css' import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
type Props = { type Props = {
content: string content: string
selectorId: string
className?: string
} }
const prefixEmbedded = 'appOverview.overview.appInfo.qrcode.title' const prefixEmbedded = 'appOverview.overview.appInfo.qrcode.title'
const ShareQRCode = ({ content, selectorId, className }: Props) => { const ShareQRCode = ({ content }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isShow, setIsShow] = useState<boolean>(false) const [isShow, setIsShow] = useState<boolean>(false)
const qrCodeRef = useRef<HTMLDivElement>(null) const qrCodeRef = useRef<HTMLDivElement>(null)
@ -53,22 +54,21 @@ const ShareQRCode = ({ content, selectorId, className }: Props) => {
<Tooltip <Tooltip
popupContent={t(`${prefixEmbedded}`) || ''} popupContent={t(`${prefixEmbedded}`) || ''}
> >
<div <div className='relative w-6 h-6' onClick={toggleQRCode}>
className={`w-8 h-8 cursor-pointer rounded-lg ${className ?? ''}`} <ActionButton>
onClick={toggleQRCode} <RiQrCodeLine className='w-4 h-4' />
> </ActionButton>
<div className={`w-full h-full ${QrcodeStyle.QrcodeIcon} ${isShow ? QrcodeStyle.show : ''}`} />
{isShow && ( {isShow && (
<div <div
ref={qrCodeRef} ref={qrCodeRef}
className={QrcodeStyle.qrcodeform} className='absolute top-8 -right-8 z-10 w-[232px] flex flex-col items-center bg-components-panel-bg shadow-xs rounded-lg p-4'
onClick={handlePanelClick} onClick={handlePanelClick}
> >
<QRCode size={160} value={content} className={QrcodeStyle.qrcodeimage}/> <QRCode size={160} value={content} className='mb-2'/>
<div className={QrcodeStyle.text}> <div className='flex items-center system-xs-regular'>
<div className={`text-gray-500 ${QrcodeStyle.scan}`}>{t('appOverview.overview.appInfo.qrcode.scan')}</div> <div className='text-text-tertiary'>{t('appOverview.overview.appInfo.qrcode.scan')}</div>
<div className={`text-gray-500 ${QrcodeStyle.scan}`}>·</div> <div className='text-text-tertiary'>·</div>
<div className={QrcodeStyle.download} onClick={downloadQR}>{t('appOverview.overview.appInfo.qrcode.download')}</div> <div className='text-text-accent-secondary cursor-pointer' onClick={downloadQR}>{t('appOverview.overview.appInfo.qrcode.download')}</div>
</div> </div>
</div> </div>
)} )}

View File

@ -1,62 +0,0 @@
.QrcodeIcon {
background-image: url(~@/app/components/develop/secret-key/assets/qrcode.svg);
background-position: center;
background-repeat: no-repeat;
}
.QrcodeIcon:hover {
background-image: url(~@/app/components/develop/secret-key/assets/qrcode-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.QrcodeIcon.show {
background-image: url(~@/app/components/develop/secret-key/assets/qrcode-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.qrcodeimage {
position: relative;
object-fit: cover;
}
.scan {
margin: 0;
line-height: 1rem;
font-size: 0.75rem;
}
.download {
position: relative;
color: #155eef;
font-size: 0.75rem;
line-height: 1rem;
}
.text {
align-self: stretch;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
white-space: nowrap;
gap: 4px;
}
.qrcodeform {
z-index: 50;
border: 0.5px solid #eaecf0;
display: flex;
flex-direction: column;
margin: 0 !important;
margin-top: 4px !important;
margin-left: -75px !important;
width: fit-content;
position: relative;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 12px 16px -4px rgba(16, 24, 40, 0.08),
0 4px 6px -2px rgba(16, 24, 40, 0.03);
overflow: hidden;
align-items: center;
justify-content: center;
padding: 15px;
gap: 8px;
}

View File

@ -393,7 +393,7 @@ const StepTwo = ({
score_threshold_enabled: false, score_threshold_enabled: false,
score_threshold: 0.5, score_threshold: 0.5,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [rerankDefaultModel, isRerankDefaultModelValid]) }, [rerankDefaultModel, isRerankDefaultModelValid])
const getCreationParams = () => { const getCreationParams = () => {
@ -412,7 +412,6 @@ const StepTwo = ({
doc_form: currentDocForm, doc_form: currentDocForm,
doc_language: docLanguage, doc_language: docLanguage,
process_rule: getProcessRule(), process_rule: getProcessRule(),
retrieval_model: retrievalConfig, // Readonly. If want to changed, just go to settings page. retrieval_model: retrievalConfig, // Readonly. If want to changed, just go to settings page.
embedding_model: embeddingModel.model, // Readonly embedding_model: embeddingModel.model, // Readonly
embedding_model_provider: embeddingModel.provider, // Readonly embedding_model_provider: embeddingModel.provider, // Readonly
@ -424,7 +423,6 @@ const StepTwo = ({
if ( if (
!isReRankModelSelected({ !isReRankModelSelected({
rerankModelList, rerankModelList,
retrievalConfig, retrievalConfig,
indexMethod: indexMethod as string, indexMethod: indexMethod as string,
}) })

View File

@ -23,7 +23,7 @@ const SecretKeyButton = ({ className, appId, iconCls, textCls }: ISecretKeyButto
<path d="M9 3.66672C9.35362 3.66672 9.69276 3.80719 9.94281 4.05724C10.1929 4.30729 10.3333 4.64643 10.3333 5.00005M13 5.00005C13.0002 5.62483 12.854 6.24097 12.5732 6.79908C12.2924 7.3572 11.8847 7.84177 11.3829 8.21397C10.8811 8.58617 10.2991 8.83564 9.68347 8.94239C9.06788 9.04915 8.43584 9.01022 7.838 8.82872L6.33333 10.3334H5V11.6667H3.66667V13.0001H1.66667C1.48986 13.0001 1.32029 12.9298 1.19526 12.8048C1.07024 12.6798 1 12.5102 1 12.3334V10.6094C1.00004 10.4326 1.0703 10.263 1.19533 10.1381L5.17133 6.16205C5.00497 5.61206 4.95904 5.03268 5.0367 4.46335C5.11435 3.89402 5.31375 3.3481 5.62133 2.86275C5.92891 2.3774 6.33744 1.96401 6.81913 1.65073C7.30082 1.33745 7.84434 1.13162 8.41272 1.04725C8.9811 0.96289 9.56098 1.00197 10.1129 1.16184C10.6648 1.32171 11.1758 1.59861 11.6111 1.97369C12.0464 2.34878 12.3958 2.81324 12.6354 3.33548C12.8751 3.85771 12.9994 4.42545 13 5.00005Z" stroke="#1F2A37" strokeLinecap="round" strokeLinejoin="round" /> <path d="M9 3.66672C9.35362 3.66672 9.69276 3.80719 9.94281 4.05724C10.1929 4.30729 10.3333 4.64643 10.3333 5.00005M13 5.00005C13.0002 5.62483 12.854 6.24097 12.5732 6.79908C12.2924 7.3572 11.8847 7.84177 11.3829 8.21397C10.8811 8.58617 10.2991 8.83564 9.68347 8.94239C9.06788 9.04915 8.43584 9.01022 7.838 8.82872L6.33333 10.3334H5V11.6667H3.66667V13.0001H1.66667C1.48986 13.0001 1.32029 12.9298 1.19526 12.8048C1.07024 12.6798 1 12.5102 1 12.3334V10.6094C1.00004 10.4326 1.0703 10.263 1.19533 10.1381L5.17133 6.16205C5.00497 5.61206 4.95904 5.03268 5.0367 4.46335C5.11435 3.89402 5.31375 3.3481 5.62133 2.86275C5.92891 2.3774 6.33744 1.96401 6.81913 1.65073C7.30082 1.33745 7.84434 1.13162 8.41272 1.04725C8.9811 0.96289 9.56098 1.00197 10.1129 1.16184C10.6648 1.32171 11.1758 1.59861 11.6111 1.97369C12.0464 2.34878 12.3958 2.81324 12.6354 3.33548C12.8751 3.85771 12.9994 4.42545 13 5.00005Z" stroke="#1F2A37" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
<div className={`text-[13px] text-gray-800 ${textCls}`}>{t('appApi.apiKey')}</div> <div className={`text-[13px] text-text-secondary ${textCls}`}>{t('appApi.apiKey')}</div>
</Button> </Button>
<SecretKeyModal isShow={isVisible} onClose={() => setVisible(false)} appId={appId} /> <SecretKeyModal isShow={isVisible} onClose={() => setVisible(false)} appId={appId} />
</> </>

View File

@ -27,7 +27,7 @@ const PluginsNav = ({
return ( return (
<Link href="/plugins" className={classNames( <Link href="/plugins" className={classNames(
className, 'group', className, 'group', 'plugins-nav-button', // used for use-fold-anim-into.ts
)}> )}>
<div <div
className={classNames( className={classNames(

View File

@ -1,6 +1,6 @@
import { sleep } from '@/utils' import { sleep } from '@/utils'
const animTime = 2000 const animTime = 750
const modalClassName = 'install-modal' const modalClassName = 'install-modal'
const COUNT_DOWN_TIME = 15000 // 15s const COUNT_DOWN_TIME = 15000 // 15s
@ -21,7 +21,7 @@ const useFoldAnimInto = (onClose: () => void) => {
const foldIntoAnim = async () => { const foldIntoAnim = async () => {
clearCountDown() clearCountDown()
const modalElem = document.querySelector(`.${modalClassName}`) as HTMLElement const modalElem = document.querySelector(`.${modalClassName}`) as HTMLElement
const pluginTaskTriggerElem = document.getElementById('plugin-task-trigger') const pluginTaskTriggerElem = document.getElementById('plugin-task-trigger') || document.querySelector('.plugins-nav-button')
if (!modalElem || !pluginTaskTriggerElem) { if (!modalElem || !pluginTaskTriggerElem) {
onClose() onClose()

View File

@ -0,0 +1,40 @@
import { useCallback, useState } from 'react'
import useFoldAnimInto from './use-fold-anim-into'
const useHideLogic = (onClose: () => void) => {
const {
modalClassName,
foldIntoAnim: doFoldAnimInto,
clearCountDown,
countDownFoldIntoAnim,
} = useFoldAnimInto(onClose)
const [isInstalling, doSetIsInstalling] = useState(false)
const setIsInstalling = useCallback((isInstalling: boolean) => {
if (!isInstalling)
clearCountDown()
doSetIsInstalling(isInstalling)
}, [clearCountDown])
const foldAnimInto = useCallback(() => {
if (isInstalling) {
doFoldAnimInto()
return
}
onClose()
}, [doFoldAnimInto, isInstalling, onClose])
const handleStartToInstall = useCallback(() => {
setIsInstalling(true)
countDownFoldIntoAnim()
}, [countDownFoldIntoAnim, setIsInstalling])
return {
modalClassName,
foldAnimInto,
setIsInstalling,
handleStartToInstall,
}
}
export default useHideLogic

View File

@ -6,6 +6,8 @@ import { InstallStep } from '../../types'
import type { Dependency } from '../../types' import type { Dependency } from '../../types'
import ReadyToInstall from './ready-to-install' import ReadyToInstall from './ready-to-install'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useHideLogic from '../hooks/use-hide-logic'
import cn from '@/utils/classnames'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -30,6 +32,13 @@ const InstallBundle: FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
const [step, setStep] = useState<InstallStep>(installType === InstallType.fromMarketplace ? InstallStep.readyToInstall : InstallStep.uploading) const [step, setStep] = useState<InstallStep>(installType === InstallType.fromMarketplace ? InstallStep.readyToInstall : InstallStep.uploading)
const {
modalClassName,
foldAnimInto,
setIsInstalling,
handleStartToInstall,
} = useHideLogic(onClose)
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
if (step === InstallStep.uploadFailed) if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`) return t(`${i18nPrefix}.uploadFailed`)
@ -42,8 +51,8 @@ const InstallBundle: FC<Props> = ({
return ( return (
<Modal <Modal
isShow={true} isShow={true}
onClose={onClose} onClose={foldAnimInto}
className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl' className={cn(modalClassName, 'flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl')}
closable closable
> >
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
@ -54,6 +63,8 @@ const InstallBundle: FC<Props> = ({
<ReadyToInstall <ReadyToInstall
step={step} step={step}
onStepChange={setStep} onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
allPlugins={fromDSLPayload} allPlugins={fromDSLPayload}
onClose={onClose} onClose={onClose}
/> />

View File

@ -9,6 +9,8 @@ import type { Dependency, InstallStatusResponse, Plugin } from '../../types'
type Props = { type Props = {
step: InstallStep step: InstallStep
onStepChange: (step: InstallStep) => void, onStepChange: (step: InstallStep) => void,
onStartToInstall: () => void
setIsInstalling: (isInstalling: boolean) => void
allPlugins: Dependency[] allPlugins: Dependency[]
onClose: () => void onClose: () => void
isFromMarketPlace?: boolean isFromMarketPlace?: boolean
@ -17,6 +19,8 @@ type Props = {
const ReadyToInstall: FC<Props> = ({ const ReadyToInstall: FC<Props> = ({
step, step,
onStepChange, onStepChange,
onStartToInstall,
setIsInstalling,
allPlugins, allPlugins,
onClose, onClose,
isFromMarketPlace, isFromMarketPlace,
@ -27,13 +31,15 @@ const ReadyToInstall: FC<Props> = ({
setInstallStatus(installStatus) setInstallStatus(installStatus)
setInstalledPlugins(plugins) setInstalledPlugins(plugins)
onStepChange(InstallStep.installed) onStepChange(InstallStep.installed)
}, [onStepChange]) setIsInstalling(false)
}, [onStepChange, setIsInstalling])
return ( return (
<> <>
{step === InstallStep.readyToInstall && ( {step === InstallStep.readyToInstall && (
<Install <Install
allPlugins={allPlugins} allPlugins={allPlugins}
onCancel={onClose} onCancel={onClose}
onStartToInstall={onStartToInstall}
onInstalled={handleInstalled} onInstalled={handleInstalled}
isFromMarketPlace={isFromMarketPlace} isFromMarketPlace={isFromMarketPlace}
/> />

View File

@ -12,6 +12,7 @@ const i18nPrefix = 'plugin.installModal'
type Props = { type Props = {
allPlugins: Dependency[] allPlugins: Dependency[]
onStartToInstall?: () => void
onInstalled: (plugins: Plugin[], installStatus: InstallStatusResponse[]) => void onInstalled: (plugins: Plugin[], installStatus: InstallStatusResponse[]) => void
onCancel: () => void onCancel: () => void
isFromMarketPlace?: boolean isFromMarketPlace?: boolean
@ -20,6 +21,7 @@ type Props = {
const Install: FC<Props> = ({ const Install: FC<Props> = ({
allPlugins, allPlugins,
onStartToInstall,
onInstalled, onInstalled,
onCancel, onCancel,
isFromMarketPlace, isFromMarketPlace,
@ -65,6 +67,7 @@ const Install: FC<Props> = ({
}, },
}) })
const handleInstall = () => { const handleInstall = () => {
onStartToInstall?.()
installOrUpdate({ installOrUpdate({
payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)), payload: allPlugins.filter((_d, index) => selectedIndexes.includes(index)),
plugin: selectedPlugins, plugin: selectedPlugins,

View File

@ -16,6 +16,8 @@ import Loaded from './steps/loaded'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
import cn from '@/utils/classnames'
import useHideLogic from '../hooks/use-hide-logic'
const i18nPrefix = 'plugin.installFromGitHub' const i18nPrefix = 'plugin.installFromGitHub'
@ -31,6 +33,13 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const { fetchReleases } = useGitHubReleases() const { fetchReleases } = useGitHubReleases()
const { refreshPluginList } = useRefreshPluginList() const { refreshPluginList } = useRefreshPluginList()
const {
modalClassName,
foldAnimInto,
setIsInstalling,
handleStartToInstall,
} = useHideLogic(onClose)
const [state, setState] = useState<InstallState>({ const [state, setState] = useState<InstallState>({
step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl, step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
repoUrl: updatePayload?.originalPackageInfo?.repo repoUrl: updatePayload?.originalPackageInfo?.repo
@ -77,13 +86,28 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
}) })
return return
} }
await fetchReleases(owner, repo).then((fetchedReleases) => { try {
setState(prevState => ({ const fetchedReleases = await fetchReleases(owner, repo)
...prevState, if (fetchedReleases.length > 0) {
releases: fetchedReleases, setState(prevState => ({
step: InstallStepFromGitHub.selectPackage, ...prevState,
})) releases: fetchedReleases,
}) step: InstallStepFromGitHub.selectPackage,
}))
}
else {
Toast.notify({
type: 'error',
message: t('plugin.error.noReleasesFound'),
})
}
}
catch (error) {
Toast.notify({
type: 'error',
message: t('plugin.error.fetchReleasesError'),
})
}
} }
const handleError = (e: any, isInstall: boolean) => { const handleError = (e: any, isInstall: boolean) => {
@ -115,14 +139,16 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const handleInstalled = useCallback(() => { const handleInstalled = useCallback(() => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
refreshPluginList(manifest) refreshPluginList(manifest)
setIsInstalling(false)
onSuccess() onSuccess()
}, [manifest, onSuccess, refreshPluginList]) }, [manifest, onSuccess, refreshPluginList, setIsInstalling])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed })) setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))
setIsInstalling(false)
if (errorMsg) if (errorMsg)
setErrorMsg(errorMsg) setErrorMsg(errorMsg)
}, []) }, [setIsInstalling])
const handleBack = () => { const handleBack = () => {
setState((prevState) => { setState((prevState) => {
@ -140,9 +166,9 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
return ( return (
<Modal <Modal
isShow={true} isShow={true}
onClose={onClose} onClose={foldAnimInto}
className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] className={cn(modalClassName, `flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px]
border-components-panel-border bg-components-panel-bg shadows-shadow-xl' border-components-panel-border bg-components-panel-bg shadows-shadow-xl`)}
closable closable
> >
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
@ -195,6 +221,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
selectedVersion={state.selectedVersion} selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage} selectedPackage={state.selectedPackage}
onBack={handleBack} onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled} onInstalled={handleInstalled}
onFailed={handleFailed} onFailed={handleFailed}
/> />

View File

@ -23,6 +23,7 @@ type LoadedProps = {
selectedVersion: string selectedVersion: string
selectedPackage: string selectedPackage: string
onBack: () => void onBack: () => void
onStartToInstall?: () => void
onInstalled: () => void onInstalled: () => void
onFailed: (message?: string) => void onFailed: (message?: string) => void
} }
@ -37,6 +38,7 @@ const Loaded: React.FC<LoadedProps> = ({
selectedVersion, selectedVersion,
selectedPackage, selectedPackage,
onBack, onBack,
onStartToInstall,
onInstalled, onInstalled,
onFailed, onFailed,
}) => { }) => {
@ -59,11 +61,13 @@ const Loaded: React.FC<LoadedProps> = ({
useEffect(() => { useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier) if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
onInstalled() onInstalled()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasInstalled]) }, [hasInstalled])
const handleInstall = async () => { const handleInstall = async () => {
if (isInstalling) return if (isInstalling) return
setIsInstalling(true) setIsInstalling(true)
onStartToInstall?.()
try { try {
const { owner, repo } = parseGitHubUrl(repoUrl) const { owner, repo } = parseGitHubUrl(repoUrl)

View File

@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import ReadyToInstallPackage from './ready-to-install' import ReadyToInstallPackage from './ready-to-install'
import ReadyToInstallBundle from '../install-bundle/ready-to-install' import ReadyToInstallBundle from '../install-bundle/ready-to-install'
import useHideLogic from '../hooks/use-hide-logic'
import cn from '@/utils/classnames'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -31,6 +33,13 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
const isBundle = file.name.endsWith('.difybndl') const isBundle = file.name.endsWith('.difybndl')
const [dependencies, setDependencies] = useState<Dependency[]>([]) const [dependencies, setDependencies] = useState<Dependency[]>([])
const {
modalClassName,
foldAnimInto,
setIsInstalling,
handleStartToInstall,
} = useHideLogic(onClose)
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
if (step === InstallStep.uploadFailed) if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`) return t(`${i18nPrefix}.uploadFailed`)
@ -76,8 +85,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
return ( return (
<Modal <Modal
isShow={true} isShow={true}
onClose={onClose} onClose={foldAnimInto}
className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl' className={cn(modalClassName, 'flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl')}
closable closable
> >
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
@ -99,6 +108,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
<ReadyToInstallBundle <ReadyToInstallBundle
step={step} step={step}
onStepChange={setStep} onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose} onClose={onClose}
allPlugins={dependencies} allPlugins={dependencies}
/> />
@ -106,6 +117,8 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
<ReadyToInstallPackage <ReadyToInstallPackage
step={step} step={step}
onStepChange={setStep} onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose} onClose={onClose}
uniqueIdentifier={uniqueIdentifier} uniqueIdentifier={uniqueIdentifier}
manifest={manifest} manifest={manifest}

View File

@ -10,6 +10,8 @@ import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
type Props = { type Props = {
step: InstallStep step: InstallStep
onStepChange: (step: InstallStep) => void, onStepChange: (step: InstallStep) => void,
onStartToInstall: () => void
setIsInstalling: (isInstalling: boolean) => void
onClose: () => void onClose: () => void
uniqueIdentifier: string | null, uniqueIdentifier: string | null,
manifest: PluginDeclaration | null, manifest: PluginDeclaration | null,
@ -20,6 +22,8 @@ type Props = {
const ReadyToInstall: FC<Props> = ({ const ReadyToInstall: FC<Props> = ({
step, step,
onStepChange, onStepChange,
onStartToInstall,
setIsInstalling,
onClose, onClose,
uniqueIdentifier, uniqueIdentifier,
manifest, manifest,
@ -31,13 +35,15 @@ const ReadyToInstall: FC<Props> = ({
const handleInstalled = useCallback(() => { const handleInstalled = useCallback(() => {
onStepChange(InstallStep.installed) onStepChange(InstallStep.installed)
refreshPluginList(manifest) refreshPluginList(manifest)
}, [manifest, onStepChange, refreshPluginList]) setIsInstalling(false)
}, [manifest, onStepChange, refreshPluginList, setIsInstalling])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
onStepChange(InstallStep.installFailed) onStepChange(InstallStep.installFailed)
setIsInstalling(false)
if (errorMsg) if (errorMsg)
onError(errorMsg) onError(errorMsg)
}, [onError, onStepChange]) }, [onError, onStepChange, setIsInstalling])
return ( return (
<> <>
@ -49,6 +55,7 @@ const ReadyToInstall: FC<Props> = ({
onCancel={onClose} onCancel={onClose}
onInstalled={handleInstalled} onInstalled={handleInstalled}
onFailed={handleFailed} onFailed={handleFailed}
onStartToInstall={onStartToInstall}
/> />
) )
} }

View File

@ -9,8 +9,8 @@ import Installed from '../base/installed'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list' import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
import ReadyToInstallBundle from '../install-bundle/ready-to-install' import ReadyToInstallBundle from '../install-bundle/ready-to-install'
import useFoldAnimInto from '../hooks/use-fold-anim-into'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useHideLogic from '../hooks/use-hide-logic'
const i18nPrefix = 'plugin.installModal' const i18nPrefix = 'plugin.installModal'
@ -39,25 +39,10 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
const { const {
modalClassName, modalClassName,
foldIntoAnim: doFoldAnimInto, foldAnimInto,
clearCountDown, setIsInstalling,
countDownFoldIntoAnim, handleStartToInstall,
} = useFoldAnimInto(onClose) } = useHideLogic(onClose)
const [isInstalling, setIsInstalling] = useState(false)
const foldAnimInto = useCallback(() => {
if (isInstalling) {
doFoldAnimInto()
return
}
onClose()
}, [doFoldAnimInto, isInstalling, onClose])
const handleStartToInstall = useCallback(() => {
setIsInstalling(true)
countDownFoldIntoAnim()
}, [countDownFoldIntoAnim])
const getTitle = useCallback(() => { const getTitle = useCallback(() => {
if (isBundle && step === InstallStep.installed) if (isBundle && step === InstallStep.installed)
@ -73,14 +58,14 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
setStep(InstallStep.installed) setStep(InstallStep.installed)
refreshPluginList(manifest) refreshPluginList(manifest)
setIsInstalling(false) setIsInstalling(false)
}, [manifest, refreshPluginList]) }, [manifest, refreshPluginList, setIsInstalling])
const handleFailed = useCallback((errorMsg?: string) => { const handleFailed = useCallback((errorMsg?: string) => {
setStep(InstallStep.installFailed) setStep(InstallStep.installFailed)
setIsInstalling(false) setIsInstalling(false)
if (errorMsg) if (errorMsg)
setErrorMsg(errorMsg) setErrorMsg(errorMsg)
}, []) }, [setIsInstalling])
return ( return (
<Modal <Modal
@ -100,6 +85,8 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
<ReadyToInstallBundle <ReadyToInstallBundle
step={step} step={step}
onStepChange={setStep} onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose} onClose={onClose}
allPlugins={dependencies!} allPlugins={dependencies!}
isFromMarketPlace isFromMarketPlace

View File

@ -144,6 +144,7 @@ const AllTools = ({
wrapElemRef={wrapElemRef} wrapElemRef={wrapElemRef}
list={notInstalledPlugins as any} ref={pluginRef} list={notInstalledPlugins as any} ref={pluginRef}
searchText={searchText} searchText={searchText}
toolContentClassName={toolContentClassName}
tags={tags} tags={tags}
/> />
</div> </div>

View File

@ -15,6 +15,7 @@ type Props = {
list: Plugin[] list: Plugin[]
searchText: string searchText: string
tags: string[] tags: string[]
toolContentClassName?: string
disableMaxWidth?: boolean disableMaxWidth?: boolean
} }
@ -23,6 +24,7 @@ const List = forwardRef<{ handleScroll: () => void }, Props>(({
searchText, searchText,
tags, tags,
list, list,
toolContentClassName,
disableMaxWidth = false, disableMaxWidth = false,
}, ref) => { }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -76,7 +78,7 @@ const List = forwardRef<{ handleScroll: () => void }, Props>(({
) )
} }
const maxWidthClassName = 'max-w-[300px]' const maxWidthClassName = toolContentClassName || 'max-w-[300px]'
return ( return (
<> <>

View File

@ -17,17 +17,19 @@ export const InstallPluginButton = (props: InstallPluginButtonProps) => {
pluginIds: [uniqueIdentifier], pluginIds: [uniqueIdentifier],
enabled: !!uniqueIdentifier, enabled: !!uniqueIdentifier,
}) })
const install = useInstallPackageFromMarketPlace({ const install = useInstallPackageFromMarketPlace()
onSuccess() { const isLoading = manifest.isLoading || install.isPending
manifest.refetch() // await for refetch to get the new installed plugin, when manifest refetch, this component will unmount
onSuccess?.() || install.isSuccess
},
})
const handleInstall: MouseEventHandler = (e) => { const handleInstall: MouseEventHandler = (e) => {
e.stopPropagation() e.stopPropagation()
install.mutate(uniqueIdentifier) install.mutate(uniqueIdentifier, {
onSuccess: async () => {
await manifest.refetch()
onSuccess?.()
},
})
} }
const isLoading = manifest.isLoading || install.isPending
if (!manifest.data) return null if (!manifest.data) return null
if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null if (manifest.data.plugins.some(plugin => plugin.id === uniqueIdentifier)) return null
return <Button return <Button

View File

@ -3,17 +3,17 @@ import Indicator from '@/app/components/header/indicator'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { memo, useMemo, useRef } from 'react' import { memo, useMemo, useRef } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import { getIconFromMarketPlace } from '@/utils/get-icon'
import { useTranslation } from 'react-i18next'
type Status = 'not-installed' | 'not-authorized' | undefined
export type ToolIconProps = { export type ToolIconProps = {
status?: 'error' | 'warning'
tooltip?: string
providerName: string providerName: string
} }
export const ToolIcon = memo(({ status, tooltip, providerName }: ToolIconProps) => { export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
const indicator = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const notSuccess = (['error', 'warning'] as Array<ToolIconProps['status']>).includes(status)
const { data: buildInTools } = useAllBuiltInTools() const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools() const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools() const { data: workflowTools } = useAllWorkflowTools()
@ -22,7 +22,29 @@ export const ToolIcon = memo(({ status, tooltip, providerName }: ToolIconProps)
return mergedTools.find((toolWithProvider) => { return mergedTools.find((toolWithProvider) => {
return toolWithProvider.name === providerName return toolWithProvider.name === providerName
}) })
}, [providerName, buildInTools, customTools, workflowTools]) }, [buildInTools, customTools, providerName, workflowTools])
const providerNameParts = providerName.split('/')
const author = providerNameParts[0]
const name = providerNameParts[1]
const icon = useMemo(() => {
if (currentProvider) return currentProvider.icon as string
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
return iconFromMarketPlace
}, [author, currentProvider, name])
const status: Status = useMemo(() => {
if (!currentProvider) return 'not-installed'
if (currentProvider.is_team_authorization === false) return 'not-authorized'
return undefined
}, [currentProvider])
const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
const { t } = useTranslation()
const tooltip = useMemo(() => {
if (!notSuccess) return undefined
if (status === 'not-installed') return t('workflow.nodes.agent.toolNotInstallTooltip', { tool: name })
if (status === 'not-authorized') return t('workflow.nodes.agent.toolNotAuthorizedTooltip', { tool: name })
throw new Error('Unknown status')
}, [name, notSuccess, status, t])
return <Tooltip triggerMethod='hover' popupContent={tooltip} disabled={!notSuccess}> return <Tooltip triggerMethod='hover' popupContent={tooltip} disabled={!notSuccess}>
<div className={classNames( <div className={classNames(
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]', 'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
@ -31,7 +53,7 @@ export const ToolIcon = memo(({ status, tooltip, providerName }: ToolIconProps)
> >
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={currentProvider?.icon as string} src={icon}
alt='tool icon' alt='tool icon'
className={classNames( className={classNames(
'w-full h-full size-3.5 object-cover', 'w-full h-full size-3.5 object-cover',

View File

@ -3,8 +3,8 @@ import produce from 'immer'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import type { ValueSelector, Var } from '../../types' import type { ValueSelector, Var } from '../../types'
import type { DocExtractorNodeType } from './types'
import { InputVarType, VarType } from '../../types' import { InputVarType, VarType } from '../../types'
import type { DocExtractorNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import { import {

View File

@ -172,6 +172,8 @@ const translation = {
}, },
error: { error: {
inValidGitHubUrl: 'Invalid GitHub URL. Please enter a valid URL in the format: https://github.com/owner/repo', inValidGitHubUrl: 'Invalid GitHub URL. Please enter a valid URL in the format: https://github.com/owner/repo',
fetchReleasesError: 'Unable to retrieve releases. Please try again later.',
noReleasesFound: 'No releases found. Please check the GitHub repository or the input URL.',
}, },
marketplace: { marketplace: {
empower: 'Empower your AI development', empower: 'Empower your AI development',

View File

@ -172,6 +172,8 @@ const translation = {
}, },
error: { error: {
inValidGitHubUrl: '无效的 GitHub URL。请输入格式为 https://github.com/owner/repo 的有效 URL', inValidGitHubUrl: '无效的 GitHub URL。请输入格式为 https://github.com/owner/repo 的有效 URL',
fetchReleasesError: '无法获取发布版本。请稍后再试。',
noReleasesFound: '未找到发布版本。请检查 GitHub 仓库或输入的 URL。',
}, },
marketplace: { marketplace: {
empower: '助力您的 AI 开发', empower: '助力您的 AI 开发',

5
web/utils/get-icon.ts Normal file
View File

@ -0,0 +1,5 @@
import { MARKETPLACE_API_PREFIX } from '@/config'
export const getIconFromMarketPlace = (plugin_id: string) => {
return `${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`
}