Merge branch 'feat/rag-2' into fix/styling-issue

This commit is contained in:
twwu 2025-09-16 11:22:39 +08:00
commit 31edc39686
28 changed files with 426 additions and 8853 deletions

View File

@ -20,7 +20,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pipeline_built_in_templates',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('pipeline_id', models.types.StringUUID(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
@ -35,7 +35,7 @@ def upgrade():
sa.PrimaryKeyConstraint('id', name='pipeline_built_in_template_pkey')
)
op.create_table('pipeline_customized_templates',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('pipeline_id', models.types.StringUUID(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
@ -52,7 +52,7 @@ def upgrade():
batch_op.create_index('pipeline_customized_template_tenant_idx', ['tenant_id'], unique=False)
op.create_table('pipelines',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), server_default=sa.text("''::character varying"), nullable=False),

View File

@ -20,7 +20,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('datasource_oauth_params',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('plugin_id', models.types.StringUUID(), nullable=False),
sa.Column('provider', sa.String(length=255), nullable=False),
sa.Column('system_credentials', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
@ -28,7 +28,7 @@ def upgrade():
sa.UniqueConstraint('plugin_id', 'provider', name='datasource_oauth_config_datasource_id_provider_idx')
)
op.create_table('datasource_providers',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('plugin_id', models.types.StringUUID(), nullable=False),
sa.Column('provider', sa.String(length=255), nullable=False),

View File

@ -20,7 +20,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('document_pipeline_execution_logs',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('pipeline_id', models.types.StringUUID(), nullable=False),
sa.Column('document_id', models.types.StringUUID(), nullable=False),
sa.Column('datasource_type', sa.String(length=255), nullable=False),

View File

@ -20,7 +20,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pipeline_recommended_plugins',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuidv7()'), nullable=False),
sa.Column('plugin_id', sa.Text(), nullable=False),
sa.Column('provider_name', sa.Text(), nullable=False),
sa.Column('position', sa.Integer(), nullable=False),

View File

@ -1225,7 +1225,7 @@ class PipelineBuiltInTemplate(Base): # type: ignore[name-defined]
__tablename__ = "pipeline_built_in_templates"
__table_args__ = (db.PrimaryKeyConstraint("id", name="pipeline_built_in_template_pkey"),)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False)
chunk_structure = db.Column(db.String(255), nullable=False)
@ -1256,7 +1256,7 @@ class PipelineCustomizedTemplate(Base): # type: ignore[name-defined]
db.Index("pipeline_customized_template_tenant_idx", "tenant_id"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
tenant_id = db.Column(StringUUID, nullable=False)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False)
@ -1283,7 +1283,7 @@ class Pipeline(Base): # type: ignore[name-defined]
__tablename__ = "pipelines"
__table_args__ = (db.PrimaryKeyConstraint("id", name="pipeline_pkey"),)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
tenant_id: Mapped[str] = db.Column(StringUUID, nullable=False)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying"))
@ -1306,7 +1306,7 @@ class DocumentPipelineExecutionLog(Base):
db.Index("document_pipeline_execution_logs_document_id_idx", "document_id"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
pipeline_id = db.Column(StringUUID, nullable=False)
document_id = db.Column(StringUUID, nullable=False)
datasource_type = db.Column(db.String(255), nullable=False)
@ -1321,7 +1321,7 @@ class PipelineRecommendedPlugin(Base):
__tablename__ = "pipeline_recommended_plugins"
__table_args__ = (db.PrimaryKeyConstraint("id", name="pipeline_recommended_plugin_pkey"),)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
plugin_id = db.Column(db.Text, nullable=False)
provider_name = db.Column(db.Text, nullable=False)
position = db.Column(db.Integer, nullable=False, default=0)

View File

@ -15,7 +15,7 @@ class DatasourceOauthParamConfig(Base): # type: ignore[name-defined]
db.UniqueConstraint("plugin_id", "provider", name="datasource_oauth_config_datasource_id_provider_idx"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
plugin_id: Mapped[str] = db.Column(db.String(255), nullable=False)
provider: Mapped[str] = db.Column(db.String(255), nullable=False)
system_credentials: Mapped[dict] = db.Column(JSONB, nullable=False)
@ -28,7 +28,7 @@ class DatasourceProvider(Base):
db.UniqueConstraint("tenant_id", "plugin_id", "provider", "name", name="datasource_provider_unique_name"),
db.Index("datasource_provider_auth_type_provider_idx", "tenant_id", "plugin_id", "provider"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
tenant_id = db.Column(StringUUID, nullable=False)
name: Mapped[str] = db.Column(db.String(255), nullable=False)
provider: Mapped[str] = db.Column(db.String(255), nullable=False)
@ -50,7 +50,7 @@ class DatasourceOauthTenantParamConfig(Base):
db.UniqueConstraint("tenant_id", "plugin_id", "provider", name="datasource_oauth_tenant_config_unique"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
id = db.Column(StringUUID, server_default=db.text("uuidv7()"))
tenant_id = db.Column(StringUUID, nullable=False)
provider: Mapped[str] = db.Column(db.String(255), nullable=False)
plugin_id: Mapped[str] = db.Column(db.String(255), nullable=False)

View File

@ -148,7 +148,11 @@ const DatasetSidebarDropdown = ({
)
})}
</nav>
<ExtraInfo relatedApps={relatedApps} expand documentCount={dataset.document_count} />
<ExtraInfo
relatedApps={relatedApps}
expand
documentCount={dataset.document_count}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.92578 11.0094C5.92578 10.0174 5.12163 9.21256 4.12956 9.21256C3.13752 9.2126 2.33333 10.0174 2.33333 11.0094C2.33349 12.0014 3.13762 12.8056 4.12956 12.8057C5.12153 12.8057 5.92562 12.0014 5.92578 11.0094ZM13.6667 11.0094C13.6667 10.0174 12.8625 9.2126 11.8704 9.21256C10.8784 9.21256 10.0742 10.0174 10.0742 11.0094C10.0744 12.0014 10.8785 12.8057 11.8704 12.8057C12.8624 12.8056 13.6665 12.0014 13.6667 11.0094ZM9.79622 4.32389C9.79619 3.33186 8.99205 2.52767 8 2.52767C7.00796 2.52767 6.20382 3.33186 6.20378 4.32389C6.20378 5.31596 7.00793 6.12012 8 6.12012C8.99207 6.12012 9.79622 5.31596 9.79622 4.32389ZM11.1296 4.32389C11.1296 5.82351 10.0748 7.07628 8.66667 7.38184V7.9196L9.74284 8.71387C10.3012 8.19607 11.0489 7.87923 11.8704 7.87923C13.5989 7.87927 15 9.28101 15 11.0094C14.9998 12.7377 13.5988 14.139 11.8704 14.139C10.1421 14.139 8.74104 12.7378 8.74089 11.0094C8.74089 10.5837 8.82585 10.1776 8.97982 9.80762L8 9.08366L7.01953 9.80762C7.17356 10.1777 7.25911 10.5836 7.25911 11.0094C7.25896 12.7378 5.85791 14.139 4.12956 14.139C2.40124 14.139 1.00016 12.7377 1 11.0094C1 9.28101 2.40114 7.87927 4.12956 7.87923C4.95094 7.87923 5.69819 8.19627 6.25651 8.71387L7.33333 7.9196V7.38184C5.92523 7.07628 4.87044 5.82351 4.87044 4.32389C4.87048 2.59548 6.27158 1.19434 8 1.19434C9.72843 1.19434 11.1295 2.59548 11.1296 4.32389Z" fill="#354052"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M5.92578 11.0094C5.92578 10.0174 5.12163 9.21256 4.12956 9.21256C3.13752 9.2126 2.33333 10.0174 2.33333 11.0094C2.33349 12.0014 3.13762 12.8056 4.12956 12.8057C5.12153 12.8057 5.92562 12.0014 5.92578 11.0094ZM13.6667 11.0094C13.6667 10.0174 12.8625 9.2126 11.8704 9.21256C10.8784 9.21256 10.0742 10.0174 10.0742 11.0094C10.0744 12.0014 10.8785 12.8057 11.8704 12.8057C12.8624 12.8056 13.6665 12.0014 13.6667 11.0094ZM9.79622 4.32389C9.79619 3.33186 8.99205 2.52767 8 2.52767C7.00796 2.52767 6.20382 3.33186 6.20378 4.32389C6.20378 5.31596 7.00793 6.12012 8 6.12012C8.99207 6.12012 9.79622 5.31596 9.79622 4.32389ZM11.1296 4.32389C11.1296 5.82351 10.0748 7.07628 8.66667 7.38184V7.9196L9.74284 8.71387C10.3012 8.19607 11.0489 7.87923 11.8704 7.87923C13.5989 7.87927 15 9.28101 15 11.0094C14.9998 12.7377 13.5988 14.139 11.8704 14.139C10.1421 14.139 8.74104 12.7378 8.74089 11.0094C8.74089 10.5837 8.82585 10.1776 8.97982 9.80762L8 9.08366L7.01953 9.80762C7.17356 10.1777 7.25911 10.5836 7.25911 11.0094C7.25896 12.7378 5.85791 14.139 4.12956 14.139C2.40124 14.139 1.00016 12.7377 1 11.0094C1 9.28101 2.40114 7.87927 4.12956 7.87923C4.95094 7.87923 5.69819 8.19627 6.25651 8.71387L7.33333 7.9196V7.38184C5.92523 7.07628 4.87044 5.82351 4.87044 4.32389C4.87048 2.59548 6.27158 1.19434 8 1.19434C9.72843 1.19434 11.1295 2.59548 11.1296 4.32389Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "ApiAggregate"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ApiAggregate.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'ApiAggregate'
export default Icon

View File

@ -1,4 +1,5 @@
export { default as AddChunks } from './AddChunks'
export { default as ApiAggregate } from './ApiAggregate'
export { default as ArrowShape } from './ArrowShape'
export { default as Chunk } from './Chunk'
export { default as Collapse } from './Collapse'

View File

@ -0,0 +1,40 @@
import React from 'react'
import type { RelatedAppResponse } from '@/models/datasets'
import Statistics from './statistics'
import ServiceApi from './service-api'
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
type IExtraInfoProps = {
relatedApps?: RelatedAppResponse
documentCount?: number
expand: boolean
}
const ExtraInfo = ({
relatedApps,
documentCount,
expand,
}: IExtraInfoProps) => {
const apiEnabled = useDatasetDetailContextWithSelector(state => state.dataset?.enable_api)
const { data: apiBaseInfo } = useDatasetApiBaseUrl()
return (
<>
{expand && (
<Statistics
expand={expand}
documentCount={documentCount}
relatedApps={relatedApps}
/>
)}
<ServiceApi
expand={expand}
apiBaseUrl={apiBaseInfo?.api_base_url ?? ''}
apiEnabled={apiEnabled ?? false}
/>
</>
)
}
export default React.memo(ExtraInfo)

View File

@ -0,0 +1,140 @@
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import cn from '@/utils/classnames'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Button from '@/app/components/base/button'
import { RiBookOpenLine, RiKey2Line } from '@remixicon/react'
import { useDisableDatasetServiceApi, useEnableDatasetServiceApi } from '@/service/knowledge/use-dataset'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Link from 'next/link'
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
type CardProps = {
apiEnabled: boolean
apiBaseUrl: string
}
const Card = ({
apiEnabled,
apiBaseUrl,
}: CardProps) => {
const { t } = useTranslation()
const datasetId = useDatasetDetailContextWithSelector(state => state.dataset?.id)
const mutateDatasetRes = useDatasetDetailContextWithSelector(state => state.mutateDatasetRes)
const { mutateAsync: enableDatasetServiceApi } = useEnableDatasetServiceApi()
const { mutateAsync: disableDatasetServiceApi } = useDisableDatasetServiceApi()
const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false)
const isCurrentWorkspaceManager = useAppContextSelector(state => state.isCurrentWorkspaceManager)
const onToggle = useCallback(async (state: boolean) => {
let result: 'success' | 'fail'
if (state)
result = (await enableDatasetServiceApi(datasetId ?? '')).result
else
result = (await disableDatasetServiceApi(datasetId ?? '')).result
if (result === 'success')
mutateDatasetRes?.()
}, [datasetId, enableDatasetServiceApi, disableDatasetServiceApi])
const handleOpenSecretKeyModal = useCallback(() => {
setIsSecretKeyModalVisible(true)
}, [])
const handleCloseSecretKeyModal = useCallback(() => {
setIsSecretKeyModalVisible(false)
}, [])
return (
<div className='flex w-[360px] flex-col rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-1'>
<div className='flex flex-col gap-y-3 p-4'>
<div className='flex items-center gap-x-3'>
<div className='flex grow items-center gap-x-2'>
<div className='flex size-6 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 shadow-md shadow-shadow-shadow-5'>
<ApiAggregate className='size-4 text-text-primary-on-surface' />
</div>
<div className='system-sm-semibold grow truncate text-text-secondary'>
{t('dataset.serviceApi.card.title')}
</div>
</div>
<div className='flex items-center gap-x-1'>
<Indicator
className='shrink-0'
color={apiEnabled ? 'green' : 'yellow'}
/>
<div
className={cn(
'system-xs-semibold-uppercase',
apiEnabled ? 'text-text-success' : 'text-text-warning',
)}
>
{apiEnabled
? t('dataset.serviceApi.enabled')
: t('dataset.serviceApi.disabled')}
</div>
</div>
<Switch
defaultValue={apiEnabled}
onChange={onToggle}
disabled={!isCurrentWorkspaceManager}
/>
</div>
<div className='flex flex-col'>
<div className='system-xs-regular leading-6 text-text-tertiary'>
{t('dataset.serviceApi.card.endpoint')}
</div>
<div className='flex h-8 items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2'>
<div className='flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1'>
<div className='system-xs-medium truncate text-text-secondary'>
{apiBaseUrl}
</div>
</div>
<CopyFeedback
content={apiBaseUrl}
/>
</div>
</div>
</div>
{/* Actions */}
<div className='flex gap-x-1 border-t-[0.5px] border-divider-subtle p-4'>
<Button
variant='ghost'
size='small'
className='gap-x-px text-text-tertiary'
onClick={handleOpenSecretKeyModal}
>
<RiKey2Line className='size-3.5 shrink-0' />
<span className='system-xs-medium px-[3px]'>
{t('dataset.serviceApi.card.apiKey')}
</span>
</Button>
<Link
href={'https://docs.dify.ai/api-reference/datasets'}
target='_blank'
rel='noopener noreferrer'
>
<Button
variant='ghost'
size='small'
className='gap-x-px text-text-tertiary'
>
<RiBookOpenLine className='size-3.5 shrink-0' />
<span className='system-xs-medium px-[3px]'>
{t('dataset.serviceApi.card.apiReference')}
</span>
</Button>
</Link>
</div>
<SecretKeyModal
isShow={isSecretKeyModalVisible}
onClose={handleCloseSecretKeyModal}
/>
</div>
)
}
export default React.memo(Card)

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import Card from './card'
type ServiceApiProps = {
expand: boolean
apiBaseUrl: string
apiEnabled: boolean
}
const ServiceApi = ({
expand,
apiBaseUrl,
apiEnabled,
}: ServiceApiProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleToggle = () => {
setOpen(!open)
}
return (
<div className='p-3 pt-2'>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='top-start'
offset={{
mainAxis: 4,
crossAxis: -4,
}}
customContainer={document.body}
>
<PortalToFollowElemTrigger
className='w-full'
onClick={handleToggle}
>
<div className={cn(
'relative flex h-8 cursor-pointer items-center gap-2 rounded-lg border border-components-panel-border px-3',
!expand && 'w-8 justify-center',
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
)}>
<ApiAggregate className='size-4 shrink-0 text-text-secondary' />
{expand && <div className='system-sm-medium grow text-text-secondary'>{t('dataset.serviceApi.title')}</div>}
<Indicator
className={cn('shrink-0', !expand && 'absolute -right-px -top-px')}
color={apiEnabled ? 'green' : 'yellow'}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<Card
apiEnabled={apiEnabled}
apiBaseUrl={apiBaseUrl}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default React.memo(ServiceApi)

View File

@ -1,33 +1,30 @@
import React from 'react'
import type { RelatedAppResponse } from '@/models/datasets'
import { useTranslation } from 'react-i18next'
import Divider from '../base/divider'
import Tooltip from '../base/tooltip'
import LinkedAppsPanel from '../base/linked-apps-panel'
import NoLinkedAppsPanel from './no-linked-apps-panel'
import Divider from '@/app/components/base/divider'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import NoLinkedAppsPanel from '../no-linked-apps-panel'
import { RiInformation2Line } from '@remixicon/react'
import type { RelatedAppResponse } from '@/models/datasets'
type IExtraInfoProps = {
relatedApps?: RelatedAppResponse
documentCount?: number
type StatisticsProps = {
expand: boolean
documentCount?: number
relatedApps?: RelatedAppResponse
}
const ExtraInfo = ({
relatedApps,
documentCount,
const Statistics = ({
expand,
}: IExtraInfoProps) => {
documentCount,
relatedApps,
}: StatisticsProps) => {
const { t } = useTranslation()
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
const relatedAppsTotal = relatedApps?.data?.length || 0
if (!expand)
return null
const relatedAppsTotal = relatedApps?.total
const hasRelatedApps = relatedApps?.data && relatedApps.data.length > 0
return (
<div className='flex items-center gap-x-0.5 p-2 pb-3'>
<div className='flex items-center gap-x-0.5 p-2 pb-0'>
<div className='flex grow flex-col px-2 pb-1.5 pt-1'>
<div className='system-md-semibold-uppercase text-text-secondary'>
{documentCount ?? '--'}
@ -66,4 +63,4 @@ const ExtraInfo = ({
)
}
export default React.memo(ExtraInfo)
export default React.memo(Statistics)

View File

@ -1,203 +0,0 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiCloseLine, RiListUnordered } from '@remixicon/react'
import TemplateEn from './template/template.en.mdx'
import TemplateZh from './template/template.zh.mdx'
import TemplateJa from './template/template.ja.mdx'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type DocProps = {
apiBaseUrl: string
}
const Doc = ({ apiBaseUrl }: DocProps) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
const [isTocExpanded, setIsTocExpanded] = useState(false)
const [activeSection, setActiveSection] = useState<string>('')
const { theme } = useTheme()
// Set initial TOC expanded state based on screen width
useEffect(() => {
const mediaQuery = window.matchMedia('(min-width: 1280px)')
setIsTocExpanded(mediaQuery.matches)
}, [])
// Extract TOC from article content
useEffect(() => {
const extractTOC = () => {
const article = document.querySelector('article')
if (article) {
const headings = article.querySelectorAll('h2')
const tocItems = Array.from(headings).map((heading) => {
const anchor = heading.querySelector('a')
if (anchor) {
return {
href: anchor.getAttribute('href') || '',
text: anchor.textContent || '',
}
}
return null
}).filter((item): item is { href: string; text: string } => item !== null)
setToc(tocItems)
// Set initial active section
if (tocItems.length > 0)
setActiveSection(tocItems[0].href.replace('#', ''))
}
}
setTimeout(extractTOC, 0)
}, [locale])
// Track scroll position for active section highlighting
useEffect(() => {
const handleScroll = () => {
const scrollContainer = document.querySelector('.scroll-container')
if (!scrollContainer || toc.length === 0)
return
// Find active section based on scroll position
let currentSection = ''
toc.forEach((item) => {
const targetId = item.href.replace('#', '')
const element = document.getElementById(targetId)
if (element) {
const rect = element.getBoundingClientRect()
// Consider section active if its top is above the middle of viewport
if (rect.top <= window.innerHeight / 2)
currentSection = targetId
}
})
if (currentSection && currentSection !== activeSection)
setActiveSection(currentSection)
}
const scrollContainer = document.querySelector('.scroll-container')
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll)
handleScroll() // Initial check
return () => scrollContainer.removeEventListener('scroll', handleScroll)
}
}, [toc, activeSection])
// Handle TOC item click
const handleTocClick = (e: React.MouseEvent<HTMLAnchorElement>, item: { href: string; text: string }) => {
e.preventDefault()
const targetId = item.href.replace('#', '')
const element = document.getElementById(targetId)
if (element) {
const scrollContainer = document.querySelector('.scroll-container')
if (scrollContainer) {
const headerOffset = -40
const elementTop = element.offsetTop - headerOffset
scrollContainer.scrollTo({
top: elementTop,
behavior: 'smooth',
})
}
}
}
const Template = useMemo(() => {
switch (locale) {
case LanguagesSupported[1]:
return <TemplateZh apiBaseUrl={apiBaseUrl} />
case LanguagesSupported[7]:
return <TemplateJa apiBaseUrl={apiBaseUrl} />
default:
return <TemplateEn apiBaseUrl={apiBaseUrl} />
}
}, [apiBaseUrl, locale])
return (
<div className='flex'>
<div className={`fixed right-20 top-32 z-10 transition-all duration-150 ease-out ${isTocExpanded ? 'w-[280px]' : 'w-11'}`}>
{isTocExpanded
? (
<nav className='toc flex max-h-[calc(100vh-150px)] w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-background-default-hover shadow-xl'>
<div className='relative z-10 flex items-center justify-between border-b border-components-panel-border-subtle bg-background-default-hover px-4 py-2.5'>
<span className='text-xs font-medium uppercase tracking-wide text-text-tertiary'>
{t('appApi.develop.toc')}
</span>
<button
onClick={() => setIsTocExpanded(false)}
className='group flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-state-base-hover'
aria-label='Close'
>
<RiCloseLine className='h-3 w-3 text-text-quaternary transition-colors group-hover:text-text-secondary' />
</button>
</div>
<div className='from-components-panel-border-subtle/20 pointer-events-none absolute left-0 right-0 top-[41px] z-10 h-2 bg-gradient-to-b to-transparent'></div>
<div className='pointer-events-none absolute left-0 right-0 top-[43px] z-10 h-3 bg-gradient-to-b from-background-default-hover to-transparent'></div>
<div className='relative flex-1 overflow-y-auto px-3 py-3 pt-1'>
{toc.length === 0 ? (
<div className='px-2 py-8 text-center text-xs text-text-quaternary'>
{t('appApi.develop.noContent')}
</div>
) : (
<ul className='space-y-0.5'>
{toc.map((item, index) => {
const isActive = activeSection === item.href.replace('#', '')
return (
<li key={index}>
<a
href={item.href}
onClick={e => handleTocClick(e, item)}
className={cn(
'group relative flex items-center rounded-md px-3 py-2 text-[13px] transition-all duration-200',
isActive
? 'bg-state-base-hover font-medium text-text-primary'
: 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
)}
>
<span
className={cn(
'mr-2 h-1.5 w-1.5 rounded-full transition-all duration-200',
isActive
? 'scale-100 bg-text-accent'
: 'scale-75 bg-components-panel-border',
)}
/>
<span className='flex-1 truncate'>
{item.text}
</span>
</a>
</li>
)
})}
</ul>
)}
</div>
<div className='pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-4 rounded-b-xl bg-gradient-to-t from-background-default-hover to-transparent'></div>
</nav>
)
: (
<button
onClick={() => setIsTocExpanded(true)}
className='group flex h-11 w-11 items-center justify-center rounded-full border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-all duration-150 hover:bg-background-default-hover hover:shadow-xl'
aria-label='Open table of contents'
>
<RiListUnordered className='h-5 w-5 text-text-tertiary transition-colors group-hover:text-text-secondary' />
</button>
)}
</div>
<article className={cn('prose-xl prose', theme === Theme.dark && 'prose-invert')}>
{Template}
</article>
</div>
)
}
export default Doc

View File

@ -1,20 +1,15 @@
'use client'
// Libraries
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useBoolean, useDebounceFn } from 'ahooks'
import { useQuery } from '@tanstack/react-query'
// Components
import ExternalAPIPanel from '../external-api/external-api-panel'
import Datasets from './datasets'
import DatasetFooter from './dataset-footer'
import ApiServer from '../../develop/ApiServer'
import Doc from './doc'
import SegmentedControl from '@/app/components/base/segmented-control'
import { RiBook2Line, RiTerminalBoxLine } from '@remixicon/react'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
import Button from '@/app/components/base/button'
@ -22,11 +17,7 @@ import Input from '@/app/components/base/input'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
// Hooks
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
@ -43,26 +34,6 @@ const List = () => {
const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
useDocumentTitle(t('dataset.knowledge'))
const options = useMemo(() => {
return [
{ value: 'dataset', text: t('dataset.datasets'), Icon: RiBook2Line },
...(currentWorkspace.role === 'dataset_operator' ? [] : [{
value: 'api', text: t('dataset.datasetsApi'), Icon: RiTerminalBoxLine,
}]),
]
}, [currentWorkspace.role, t])
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'dataset',
})
const { data } = useQuery(
{
queryKey: ['datasetApiBaseInfo'],
queryFn: () => fetchDatasetApiBaseUrl('/datasets/api-base-info'),
enabled: activeTab !== 'dataset',
},
)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { run: handleSearch } = useDebounceFn(() => {
@ -89,55 +60,42 @@ const List = () => {
return (
<div className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
<div className='sticky top-0 z-10 flex items-center justify-between gap-x-1 bg-background-body px-12 pb-2 pt-4'>
<SegmentedControl
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab as string)}
options={options}
size='large'
activeClassName='text-text-primary'
/>
{activeTab === 'dataset' && (
<div className='flex items-center justify-center gap-2'>
{isCurrentWorkspaceOwner && <CheckboxWithLabel
<div className='sticky top-0 z-10 flex items-center justify-end gap-x-1 bg-background-body px-12 pb-2 pt-4'>
<div className='flex items-center justify-center gap-2'>
{isCurrentWorkspaceOwner && (
<CheckboxWithLabel
isChecked={includeAll}
onChange={toggleIncludeAll}
label={t('dataset.allKnowledge')}
labelClassName='system-md-regular text-text-secondary'
className='mr-2'
tooltip={t('dataset.allKnowledgeDescription') as string}
/>}
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
<div className='h-4 w-[1px] bg-divider-regular' />
<Button
className='shadows-shadow-xs gap-0.5'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' />
<div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div>
</Button>
</div>
)}
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
</div>
{activeTab === 'dataset' && (
<>
<Datasets tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
{!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}
</>
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
<div className='h-4 w-[1px] bg-divider-regular' />
<Button
className='shadows-shadow-xs gap-0.5'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' />
<div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div>
</Button>
</div>
</div>
<Datasets tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
{!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
{showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} />}
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -46,14 +46,14 @@ export default function Indicator({
className = '',
}: IndicatorProps) {
return (
<div className={classNames(
'h-2 w-2 rounded-[3px] border border-solid',
BACKGROUND_MAP[color],
BORDER_MAP[color],
SHADOW_MAP[color],
className,
)}>
</div>
<div
className={classNames(
'h-2 w-2 rounded-[3px] border border-solid',
BACKGROUND_MAP[color],
BORDER_MAP[color],
SHADOW_MAP[color],
className,
)}
/>
)
}

View File

@ -6,6 +6,7 @@ import dataSourceDefault from '@/app/components/workflow/nodes/data-source/defau
import dataSourceEmptyDefault from '@/app/components/workflow/nodes/data-source-empty/default'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import { BlockEnum } from '@/app/components/workflow/types'
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
@ -61,7 +62,10 @@ export const useAvailableNodesMetaData = () => {
return useMemo(() => {
return {
nodes: availableNodesMetaData,
nodesMap: availableNodesMetaDataMap,
nodesMap: {
...availableNodesMetaDataMap,
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
},
}
}, [availableNodesMetaData, availableNodesMetaDataMap])
}

View File

@ -7,6 +7,7 @@ import AnswerDefault from '@/app/components/workflow/nodes/answer/default'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import { useIsChatMode } from './use-is-chat-mode'
import { BlockEnum } from '@/app/components/workflow/types'
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
@ -58,7 +59,10 @@ export const useAvailableNodesMetaData = () => {
return useMemo(() => {
return {
nodes: availableNodesMetaData,
nodesMap: availableNodesMetaDataMap,
nodesMap: {
...availableNodesMetaDataMap,
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
},
}
}, [availableNodesMetaData, availableNodesMetaDataMap])
}

View File

@ -226,6 +226,17 @@ const translation = {
technicalParameters: 'Technical Parameters',
},
},
serviceApi: {
title: 'Service API',
enabled: 'In Service',
disabled: 'Disabled',
card: {
title: 'Backend service api',
endpoint: 'Service API Endpoint',
apiKey: 'API Key',
apiReference: 'API Reference',
},
},
}
export default translation

View File

@ -226,6 +226,17 @@ const translation = {
technicalParameters: '技术参数',
},
},
serviceApi: {
title: '服务 API',
enabled: '运行中',
disabled: '已停用',
card: {
title: '后端服务 API',
endpoint: 'API 端点',
apiKey: 'API 密钥',
apiReference: 'API 文档',
},
},
}
export default translation

View File

@ -84,6 +84,7 @@ export type DataSet = {
pipeline_id?: string
is_published?: boolean // Indicates if the pipeline is published
runtime_mode: 'rag_pipeline' | 'general'
enable_api: boolean
}
export type ExternalAPIItem = {

View File

@ -203,10 +203,6 @@ export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Re
return post<CreateApiKeyResponse>(url, body)
}
export const fetchDatasetApiBaseUrl: Fetcher<{ api_base_url: string }, string> = (url) => {
return get<{ api_base_url: string }>(url)
}
export const fetchDataSources = () => {
return get<CommonResponse>('api-key-auth/data-source')
}

View File

@ -9,9 +9,10 @@ import type {
ProcessRuleResponse,
RelatedAppResponse,
} from '@/models/datasets'
import { get } from '../base'
import { get, post } from '../base'
import { useInvalid } from '../use-base'
import qs from 'qs'
import type { CommonResponse } from '@/models/common'
const NAME_SPACE = 'dataset'
@ -75,3 +76,24 @@ export const useProcessRule = (documentId: string) => {
queryFn: () => get<ProcessRuleResponse>('/datasets/process-rule', { params: { document_id: documentId } }),
})
}
export const useDatasetApiBaseUrl = () => {
return useQuery<{ api_base_url: string }>({
queryKey: [NAME_SPACE, 'api-base-info'],
queryFn: () => get<{ api_base_url: string }>('/datasets/api-base-info'),
})
}
export const useEnableDatasetServiceApi = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable-api'],
mutationFn: (datasetId: string) => post<CommonResponse>(`/datasets/${datasetId}/enable`),
})
}
export const useDisableDatasetServiceApi = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable-api'],
mutationFn: (datasetId: string) => post<CommonResponse>(`/datasets/${datasetId}/disable`),
})
}