Compare commits

..

3 Commits

Author SHA1 Message Date
Joel
19de28aeea chore: remove chinese comment 2025-12-02 22:09:31 +08:00
Joel
5012685aa9 feat: detect all files 2025-12-02 16:50:07 +08:00
Joel
c269b044c3 feat: detect the cognitive-complexity of file 2025-12-02 15:43:33 +08:00
24 changed files with 509 additions and 2053 deletions

View File

@@ -1004,11 +1004,6 @@ class RagPipelineRecommendedPluginApi(Resource):
@login_required
@account_initialization_required
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('type', type=str, location='args', required=False, default='all')
args = parser.parse_args()
type = args["type"]
rag_pipeline_service = RagPipelineService()
recommended_plugins = rag_pipeline_service.get_recommended_plugins(type)
recommended_plugins = rag_pipeline_service.get_recommended_plugins()
return recommended_plugins

View File

@@ -1,64 +0,0 @@
"""Alter table pipeline_recommended_plugins add column type
Revision ID: 6bb0832495f0
Revises: 7bb281b7a422
Create Date: 2025-12-15 16:14:38.482072
"""
from alembic import op
import models as models
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '6bb0832495f0'
down_revision = '7bb281b7a422'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('app_triggers', schema=None) as batch_op:
batch_op.alter_column('provider_name',
existing_type=sa.VARCHAR(length=255),
nullable=False,
existing_server_default=sa.text("''::character varying"))
with op.batch_alter_table('operation_logs', schema=None) as batch_op:
batch_op.alter_column('content',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=False)
with op.batch_alter_table('pipeline_recommended_plugins', schema=None) as batch_op:
batch_op.add_column(sa.Column('type', sa.String(length=50), nullable=True))
with op.batch_alter_table('providers', schema=None) as batch_op:
batch_op.alter_column('quota_used',
existing_type=sa.BIGINT(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('providers', schema=None) as batch_op:
batch_op.alter_column('quota_used',
existing_type=sa.BIGINT(),
nullable=True)
with op.batch_alter_table('pipeline_recommended_plugins', schema=None) as batch_op:
batch_op.drop_column('type')
with op.batch_alter_table('operation_logs', schema=None) as batch_op:
batch_op.alter_column('content',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=True)
with op.batch_alter_table('app_triggers', schema=None) as batch_op:
batch_op.alter_column('provider_name',
existing_type=sa.VARCHAR(length=255),
nullable=True,
existing_server_default=sa.text("''::character varying"))
# ### end Alembic commands ###

View File

@@ -1458,7 +1458,6 @@ class PipelineRecommendedPlugin(TypeBase):
)
plugin_id: Mapped[str] = mapped_column(LongText, nullable=False)
provider_name: Mapped[str] = mapped_column(LongText, nullable=False)
type: Mapped[str] = mapped_column(sa.String(50), nullable=True)
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
active: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(

View File

@@ -19,7 +19,7 @@ class StringUUID(TypeDecorator[uuid.UUID | str | None]):
def process_bind_param(self, value: uuid.UUID | str | None, dialect: Dialect) -> str | None:
if value is None:
return value
elif dialect.name in ["postgresql", "mysql"]:
elif dialect.name == "postgresql":
return str(value)
else:
if isinstance(value, uuid.UUID):

View File

@@ -907,29 +907,19 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo
@property
def extras(self) -> dict[str, Any]:
from core.tools.tool_manager import ToolManager
from core.trigger.trigger_manager import TriggerManager
extras: dict[str, Any] = {}
execution_metadata = self.execution_metadata_dict
if execution_metadata:
if self.node_type == NodeType.TOOL and "tool_info" in execution_metadata:
tool_info: dict[str, Any] = execution_metadata["tool_info"]
if self.execution_metadata_dict:
if self.node_type == NodeType.TOOL and "tool_info" in self.execution_metadata_dict:
tool_info: dict[str, Any] = self.execution_metadata_dict["tool_info"]
extras["icon"] = ToolManager.get_tool_icon(
tenant_id=self.tenant_id,
provider_type=tool_info["provider_type"],
provider_id=tool_info["provider_id"],
)
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in execution_metadata:
datasource_info = execution_metadata["datasource_info"]
elif self.node_type == NodeType.DATASOURCE and "datasource_info" in self.execution_metadata_dict:
datasource_info = self.execution_metadata_dict["datasource_info"]
extras["icon"] = datasource_info.get("icon")
elif self.node_type == NodeType.TRIGGER_PLUGIN and "trigger_info" in execution_metadata:
trigger_info = execution_metadata["trigger_info"] or {}
provider_id = trigger_info.get("provider_id")
if provider_id:
extras["icon"] = TriggerManager.get_trigger_plugin_icon(
tenant_id=self.tenant_id,
provider_id=provider_id,
)
return extras
def _get_offload_by_type(self, type_: ExecutionOffLoadType) -> Optional["WorkflowNodeExecutionOffload"]:

View File

@@ -1248,14 +1248,12 @@ class RagPipelineService:
session.commit()
return workflow_node_execution_db_model
def get_recommended_plugins(self, type: str) -> dict:
def get_recommended_plugins(self) -> dict:
# Query active recommended plugins
query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
if type and type != "all":
query = query.where(PipelineRecommendedPlugin.type == type)
pipeline_recommended_plugins = (
query.order_by(PipelineRecommendedPlugin.position.asc())
db.session.query(PipelineRecommendedPlugin)
.where(PipelineRecommendedPlugin.active == True)
.order_by(PipelineRecommendedPlugin.position.asc())
.all()
)

File diff suppressed because it is too large Load Diff

1
web/.gitignore vendored
View File

@@ -54,3 +54,4 @@ package-lock.json
# mise
mise.toml
complexity-report.csv

View File

@@ -1,5 +1,6 @@
'use client'
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
RiGraduationCapFill,
@@ -22,9 +23,8 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
import EmailChangeModal from './email-change-modal'
import { validPassword } from '@/config'
import { fetchAppList } from '@/service/apps'
import type { App } from '@/types/app'
import { useAppList } from '@/service/use-apps'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -36,7 +36,7 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
const { data: appList } = useSWR({ url: '/apps', params: { page: 1, limit: 100, name: '' } }, fetchAppList)
const apps = appList?.data || []
const { mutateUserProfile, userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import ReactECharts from 'echarts-for-react'
import type { EChartsOption } from 'echarts'
import useSWR from 'swr'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { get } from 'lodash-es'
@@ -12,20 +13,7 @@ import { formatNumber } from '@/utils/format'
import Basic from '@/app/components/app-sidebar/basic'
import Loading from '@/app/components/base/loading'
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppTokenCostsResponse } from '@/models/app'
import {
useAppAverageResponseTime,
useAppAverageSessionInteractions,
useAppDailyConversations,
useAppDailyEndUsers,
useAppDailyMessages,
useAppSatisfactionRate,
useAppTokenCosts,
useAppTokensPerSecond,
useWorkflowAverageInteractions,
useWorkflowDailyConversations,
useWorkflowDailyTerminals,
useWorkflowTokenCosts,
} from '@/service/use-apps'
import { getAppDailyConversations, getAppDailyEndUsers, getAppDailyMessages, getAppStatistics, getAppTokenCosts, getWorkflowDailyConversations } from '@/service/apps'
const valueFormatter = (v: string | number) => v
const COLOR_TYPE_MAP = {
@@ -284,8 +272,8 @@ const getDefaultChartData = ({ start, end, key = 'count' }: { start: string; end
export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppDailyMessages(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-messages`, params: period.query }, getAppDailyMessages)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -298,8 +286,8 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppDailyConversations(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -313,8 +301,8 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppDailyEndUsers(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -327,8 +315,8 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppAverageSessionInteractions(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-session-interactions`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -343,8 +331,8 @@ export const AvgSessionInteractions: FC<IBizChartProps> = ({ id, period }) => {
export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppAverageResponseTime(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/average-response-time`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -360,8 +348,8 @@ export const AvgResponseTime: FC<IBizChartProps> = ({ id, period }) => {
export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppTokensPerSecond(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/tokens-per-second`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -378,8 +366,8 @@ export const TokenPerSecond: FC<IBizChartProps> = ({ id, period }) => {
export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppSatisfactionRate(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/user-satisfaction-rate`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -396,8 +384,8 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useAppTokenCosts(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -410,8 +398,8 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useWorkflowDailyConversations(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-conversations`, params: period.query }, getWorkflowDailyConversations)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -426,8 +414,8 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useWorkflowDailyTerminals(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/daily-terminals`, id, params: period.query }, getAppDailyEndUsers)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -441,8 +429,8 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useWorkflowTokenCosts(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/token-costs`, params: period.query }, getAppTokenCosts)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart
@@ -455,8 +443,8 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
const { t } = useTranslation()
const { data: response, isLoading } = useWorkflowAverageInteractions(id, period.query)
if (isLoading || !response)
const { data: response } = useSWR({ url: `/apps/${id}/workflow/statistics/average-app-interactions`, params: period.query }, getAppStatistics)
if (!response)
return <Loading />
const noDataFlag = !response.data || response.data.length === 0
return <Chart

View File

@@ -23,7 +23,7 @@ const Empty = () => {
return (
<>
<DefaultCards />
<div className='pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
<div className='absolute inset-0 z-20 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent pointer-events-none'>
<span className='system-md-medium text-text-tertiary'>
{t('app.newApp.noAppsFound')}
</span>

View File

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import {
useRouter,
} from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import {
@@ -18,6 +19,8 @@ import AppCard from './app-card'
import NewAppCard from './new-app-card'
import useAppsQueryState from './hooks/use-apps-query-state'
import { useDSLDragDrop } from './hooks/use-dsl-drag-drop'
import type { AppListResponse } from '@/models/app'
import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
@@ -32,7 +35,6 @@ import Empty from './empty'
import Footer from './footer'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AppModeEnum } from '@/types/app'
import { useInfiniteAppList } from '@/service/use-apps'
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
ssr: false,
@@ -41,6 +43,30 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
ssr: false,
})
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
isCreatedByMe: boolean,
tags: string[],
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords, is_created_by_me: isCreatedByMe } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
if (tags.length)
params.params.tag_ids = tags
return params
}
return null
}
const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
@@ -76,24 +102,16 @@ const List = () => {
enabled: isCurrentWorkspaceEditor,
})
const appListQueryParams = {
page: 1,
limit: 30,
name: searchKeywords,
tag_ids: tagIDs,
is_created_by_me: isCreatedByMe,
...(activeTab !== 'all' ? { mode: activeTab as AppModeEnum } : {}),
}
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
error,
refetch,
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
@@ -108,9 +126,9 @@ const List = () => {
useEffect(() => {
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
refetch()
mutate()
}
}, [refetch])
}, [mutate, t])
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
@@ -118,9 +136,7 @@ const List = () => {
}, [router, isCurrentWorkspaceDatasetOperator])
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
return
const hasMore = hasNextPage ?? true
const hasMore = data?.at(-1)?.has_more ?? true
let observer: IntersectionObserver | undefined
if (error) {
@@ -135,8 +151,8 @@ const List = () => {
const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) // Clamps to 100-200px range, using 20% of container height as the base value
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore)
fetchNextPage()
if (entries[0].isIntersecting && !isLoading && !error && hasMore)
setSize((size: number) => size + 1)
}, {
root: containerRef.current,
rootMargin: `${dynamicMargin}px`,
@@ -145,7 +161,7 @@ const List = () => {
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, isFetchingNextPage, fetchNextPage, error, hasNextPage, isCurrentWorkspaceDatasetOperator])
}, [isLoading, setSize, data, error])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
@@ -169,9 +185,6 @@ const List = () => {
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
}, [isCreatedByMe, setQuery])
const pages = data?.pages ?? []
const hasAnyApp = (pages[0]?.total ?? 0) > 0
return (
<>
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
@@ -204,17 +217,17 @@ const List = () => {
/>
</div>
</div>
{hasAnyApp
{(data && data[0].total > 0)
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} selectedAppType={activeTab} />}
{data.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
</div>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={refetch} selectedAppType={activeTab} />}
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} selectedAppType={activeTab} />}
<Empty />
</div>}
@@ -248,7 +261,7 @@ const List = () => {
onSuccess={() => {
setShowCreateFromDSLModal(false)
setDroppedDSLFile(undefined)
refetch()
mutate()
}}
droppedFile={droppedDSLFile}
/>

View File

@@ -1,4 +1,5 @@
'use client'
import useSWR from 'swr'
import { produce } from 'immer'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
@@ -8,6 +9,7 @@ import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } fro
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { Item } from '@/app/components/base/select'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import AudioBtn from '@/app/components/base/audio-btn'
@@ -15,7 +17,6 @@ import { languages } from '@/i18n-config/language'
import { TtsAutoPlay } from '@/types/app'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import classNames from '@/utils/classnames'
import { useAppVoices } from '@/service/use-apps'
type VoiceParamConfigProps = {
onClose: () => void
@@ -38,7 +39,7 @@ const VoiceParamConfig = ({
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
const language = languageItem?.value
const { data: voiceItems } = useAppVoices(appId, language)
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
if (voiceItems && !voiceItem)
voiceItem = voiceItems[0]

View File

@@ -5,7 +5,7 @@ import {
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine } from '@remixicon/react'
import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import useSWR from 'swr'
import useSWR, { useSWRConfig } from 'swr'
import SecretKeyGenerateModal from './secret-key-generate'
import s from './style.module.css'
import ActionButton from '@/app/components/base/action-button'
@@ -15,6 +15,7 @@ import CopyFeedback from '@/app/components/base/copy-feedback'
import {
createApikey as createAppApikey,
delApikey as delAppApikey,
fetchApiKeysList as fetchAppApiKeysList,
} from '@/service/apps'
import {
createApikey as createDatasetApikey,
@@ -26,7 +27,6 @@ import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import useTimestamp from '@/hooks/use-timestamp'
import { useAppContext } from '@/context/app-context'
import { useAppApiKeys, useInvalidateAppApiKeys } from '@/service/use-apps'
type ISecretKeyModalProps = {
isShow: boolean
@@ -45,14 +45,12 @@ const SecretKeyModal = ({
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setVisible] = useState(false)
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
const invalidateAppApiKeys = useInvalidateAppApiKeys()
const { data: appApiKeys, isLoading: isAppApiKeysLoading } = useAppApiKeys(appId, { enabled: !!appId && isShow })
const { data: datasetApiKeys, isLoading: isDatasetApiKeysLoading, mutate: mutateDatasetApiKeys } = useSWR(
!appId && isShow ? { url: '/datasets/api-keys', params: {} } : null,
fetchDatasetApiKeysList,
)
const apiKeysList = appId ? appApiKeys : datasetApiKeys
const isApiKeysLoading = appId ? isAppApiKeysLoading : isDatasetApiKeysLoading
const { mutate } = useSWRConfig()
const commonParams = appId
? { url: `/apps/${appId}/api-keys`, params: {} }
: { url: '/datasets/api-keys', params: {} }
const fetchApiKeysList = appId ? fetchAppApiKeysList : fetchDatasetApiKeysList
const { data: apiKeysList } = useSWR(commonParams, fetchApiKeysList)
const [delKeyID, setDelKeyId] = useState('')
@@ -66,10 +64,7 @@ const SecretKeyModal = ({
? { url: `/apps/${appId}/api-keys/${delKeyID}`, params: {} }
: { url: `/datasets/api-keys/${delKeyID}`, params: {} }
await delApikey(params)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
mutate(commonParams)
}
const onCreate = async () => {
@@ -80,10 +75,7 @@ const SecretKeyModal = ({
const res = await createApikey(params)
setVisible(true)
setNewKey(res)
if (appId)
invalidateAppApiKeys(appId)
else
mutateDatasetApiKeys()
mutate(commonParams)
}
const generateToken = (token: string) => {
@@ -96,7 +88,7 @@ const SecretKeyModal = ({
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className='mt-1 shrink-0 text-[13px] font-normal leading-5 text-text-tertiary'>{t('appApi.apiKeyModal.apiSecretKeyTips')}</p>
{isApiKeysLoading && <div className='mt-4'><Loading /></div>}
{!apiKeysList && <div className='mt-4'><Loading /></div>}
{
!!apiKeysList?.data?.length && (
<div className='mt-4 flex grow flex-col overflow-hidden'>

View File

@@ -214,12 +214,8 @@ export const searchAnything = async (
actionItem?: ActionItem,
dynamicActions?: Record<string, ActionItem>,
): Promise<SearchResult[]> => {
const trimmedQuery = query.trim()
if (actionItem) {
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const prefixPattern = new RegExp(`^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`)
const searchTerm = trimmedQuery.replace(prefixPattern, '').trim()
const searchTerm = query.replace(actionItem.key, '').replace(actionItem.shortcut, '').trim()
try {
return await actionItem.search(query, searchTerm, locale)
}
@@ -229,12 +225,10 @@ export const searchAnything = async (
}
}
if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/'))
if (query.startsWith('@') || query.startsWith('/'))
return []
const globalSearchActions = Object.values(dynamicActions || Actions)
// Exclude slash commands from general search results
.filter(action => action.key !== '/')
// Use Promise.allSettled to handle partial failures gracefully
const searchPromises = globalSearchActions.map(async (action) => {

View File

@@ -177,42 +177,31 @@ const GotoAnything: FC<Props> = ({
}
}, [router])
const dedupedResults = useMemo(() => {
const seen = new Set<string>()
return searchResults.filter((result) => {
const key = `${result.type}-${result.id}`
if (seen.has(key))
return false
seen.add(key)
return true
})
}, [searchResults])
// Group results by type
const groupedResults = useMemo(() => dedupedResults.reduce((acc, result) => {
const groupedResults = useMemo(() => searchResults.reduce((acc, result) => {
if (!acc[result.type])
acc[result.type] = []
acc[result.type].push(result)
return acc
}, {} as { [key: string]: SearchResult[] }),
[dedupedResults])
[searchResults])
useEffect(() => {
if (isCommandsMode)
return
if (!dedupedResults.length)
if (!searchResults.length)
return
const currentValueExists = dedupedResults.some(result => `${result.type}-${result.id}` === cmdVal)
const currentValueExists = searchResults.some(result => `${result.type}-${result.id}` === cmdVal)
if (!currentValueExists)
setCmdVal(`${dedupedResults[0].type}-${dedupedResults[0].id}`)
}, [isCommandsMode, dedupedResults, cmdVal])
setCmdVal(`${searchResults[0].type}-${searchResults[0].id}`)
}, [isCommandsMode, searchResults, cmdVal])
const emptyResult = useMemo(() => {
if (dedupedResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null
const isCommandSearch = searchMode !== 'general'
@@ -257,7 +246,7 @@ const GotoAnything: FC<Props> = ({
</div>
</div>
)
}, [dedupedResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
const defaultUI = useMemo(() => {
if (searchQuery.trim())
@@ -441,14 +430,14 @@ const GotoAnything: FC<Props> = ({
{/* Always show footer to prevent height jumping */}
<div className='border-t border-divider-subtle bg-components-panel-bg-blur px-4 py-2 text-xs text-text-tertiary'>
<div className='flex min-h-[16px] items-center justify-between'>
{(!!dedupedResults.length || isError) ? (
{(!!searchResults.length || isError) ? (
<>
<span>
{isError ? (
<span className='text-red-500'>{t('app.gotoAnything.someServicesUnavailable')}</span>
) : (
<>
{t('app.gotoAnything.resultCount', { count: dedupedResults.length })}
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
{searchMode !== 'general' && (
<span className='ml-2 opacity-60'>
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}

View File

@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { flatten } from 'lodash-es'
import { produce } from 'immer'
import {
@@ -11,13 +12,33 @@ import {
} from '@remixicon/react'
import Nav from '../nav'
import type { NavItem } from '../nav/nav-selector'
import { fetchAppList } from '@/service/apps'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import type { AppListResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AppModeEnum } from '@/types/app'
import { useInfiniteAppList } from '@/service/use-apps'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
if (activeTab !== 'all')
params.params.mode = activeTab
else
delete params.params.mode
return params
}
return null
}
const AppNav = () => {
const { t } = useTranslation()
@@ -29,21 +50,17 @@ const AppNav = () => {
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
const [navItems, setNavItems] = useState<NavItem[]>([])
const {
data: appsData,
fetchNextPage,
hasNextPage,
refetch,
} = useInfiniteAppList({
page: 1,
limit: 30,
name: '',
}, { enabled: !!appId })
const { data: appsData, setSize, mutate } = useSWRInfinite(
appId
? (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, 'all', '')
: () => null,
fetchAppList,
{ revalidateFirstPage: false },
)
const handleLoadMore = useCallback(() => {
if (hasNextPage)
fetchNextPage()
}, [fetchNextPage, hasNextPage])
setSize(size => size + 1)
}, [setSize])
const openModal = (state: string) => {
if (state === 'blank')
@@ -56,7 +73,7 @@ const AppNav = () => {
useEffect(() => {
if (appsData) {
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
const appItems = flatten(appsData?.map(appData => appData.data))
const navItems = appItems.map((app) => {
const link = ((isCurrentWorkspaceEditor, app) => {
if (!isCurrentWorkspaceEditor) {
@@ -115,17 +132,17 @@ const AppNav = () => {
<CreateAppModal
show={showNewAppDialog}
onClose={() => setShowNewAppDialog(false)}
onSuccess={() => refetch()}
onSuccess={() => mutate()}
/>
<CreateAppTemplateDialog
show={showNewAppTemplateDialog}
onClose={() => setShowNewAppTemplateDialog(false)}
onSuccess={() => refetch()}
onSuccess={() => mutate()}
/>
<CreateFromDSLModal
show={showCreateFromDSLModal}
onClose={() => setShowCreateFromDSLModal(false)}
onSuccess={() => refetch()}
onSuccess={() => mutate()}
/>
</>
)

View File

@@ -15,10 +15,32 @@ import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import { useInfiniteAppList } from '@/service/use-apps'
import useSWRInfinite from 'swr/infinite'
import { fetchAppList } from '@/service/apps'
import type { AppListResponse } from '@/models/app'
const PAGE_SIZE = 20
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
searchText: string,
) => {
if (pageIndex === 0 || (previousPageData && previousPageData.has_more)) {
const params: any = {
url: 'apps',
params: {
page: pageIndex + 1,
limit: PAGE_SIZE,
name: searchText,
},
}
return params
}
return null
}
type Props = {
value?: {
app_id: string
@@ -50,32 +72,30 @@ const AppSelector: FC<Props> = ({
const [searchText, setSearchText] = useState('')
const [isLoadingMore, setIsLoadingMore] = useState(false)
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteAppList({
page: 1,
limit: PAGE_SIZE,
name: searchText,
})
const { data, isLoading, setSize } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText),
fetchAppList,
{
revalidateFirstPage: true,
shouldRetryOnError: false,
dedupingInterval: 500,
errorRetryCount: 3,
},
)
const pages = data?.pages ?? []
const displayedApps = useMemo(() => {
if (!pages.length) return []
return pages.flatMap(({ data: apps }) => apps)
}, [pages])
if (!data) return []
return data.flatMap(({ data: apps }) => apps)
}, [data])
const hasMore = hasNextPage ?? true
const hasMore = data?.at(-1)?.has_more ?? true
const handleLoadMore = useCallback(async () => {
if (isLoadingMore || isFetchingNextPage || !hasMore) return
if (isLoadingMore || !hasMore) return
setIsLoadingMore(true)
try {
await fetchNextPage()
await setSize((size: number) => size + 1)
}
finally {
// Add a small delay to ensure state updates are complete
@@ -83,7 +103,7 @@ const AppSelector: FC<Props> = ({
setIsLoadingMore(false)
}, 300)
}
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
}, [isLoadingMore, hasMore, setSize])
const handleTriggerClick = () => {
if (disabled) return
@@ -165,7 +185,7 @@ const AppSelector: FC<Props> = ({
onSelect={handleSelectApp}
scope={scope || 'all'}
apps={displayedApps}
isLoading={isLoading || isLoadingMore || isFetchingNextPage}
isLoading={isLoading || isLoadingMore}
hasMore={hasMore}
onLoadMore={handleLoadMore}
searchText={searchText}

128
web/calculate-complexity.js Normal file
View File

@@ -0,0 +1,128 @@
// https://www.sonarsource.com/blog/5-clean-code-tips-for-reducing-cognitive-complexity/
const fs = require('fs');
const path = require('path');
const { Linter } = require('eslint');
const sonarPlugin = require('eslint-plugin-sonarjs');
const tsParser = require('@typescript-eslint/parser');
const linter = new Linter();
const config = {
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
}
},
plugins: {
sonarjs: sonarPlugin,
},
rules: {
'sonarjs/cognitive-complexity': ['error', 0], // always show error
},
};
function getFileComplexity(filePath) {
try {
const code = fs.readFileSync(filePath, 'utf8');
const messages = linter.verify(code, config);
let totalFileComplexity = 0;
let maxFileComplexityIndex = 0;
const functionComplexities = [];
messages.forEach((msg) => {
// console.log(msg);
if (msg.ruleId === 'sonarjs/cognitive-complexity') {
const match = msg.message.match(/reduce its Cognitive Complexity from (\d+)/);
if (match && match[1]) {
const score = parseInt(match[1], 10);
totalFileComplexity += score;
if (score > functionComplexities[maxFileComplexityIndex]?.score || functionComplexities.length === 0) {
maxFileComplexityIndex = functionComplexities.length;
}
functionComplexities.push({
line: msg.line,
// functionName: extractFunctionName(code, msg.line),
score: score,
// message: msg.message
});
}
}
});
return {
file: filePath,
totalComplexity: totalFileComplexity,
maxComplexityInfo: functionComplexities[maxFileComplexityIndex],
details: functionComplexities
};
} catch (error) {
console.error(`Error processing file ${filePath}:`, error);
return null;
}
}
function collectTsxFiles(baseDir) {
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
const files = [];
entries.forEach((entry) => {
const fullPath = path.join(baseDir, entry.name);
if (entry.isDirectory()) {
if (
entry.name === 'node_modules' ||
entry.name.startsWith('.') ||
entry.name === '__test__' ||
entry.name === '__tests__'
) {
return;
}
files.push(...collectTsxFiles(fullPath));
} else if (
entry.isFile() &&
entry.name.endsWith('.tsx') &&
!entry.name.endsWith('.spec.tsx') &&
!entry.name.endsWith('.test.tsx')
) {
files.push(fullPath);
}
});
return files;
}
function writeCsv(results, outputPath) {
const header = 'File,Total Complexity,Max Complexity,Max Complexity Line';
const rows = results.map(({ file, totalComplexity, maxComplexityInfo }) => {
const maxScore = maxComplexityInfo?.score ?? 0;
const maxLine = maxComplexityInfo?.line ?? '';
const targetFile = JSON.stringify(path.relative(process.cwd(), file));
return `${targetFile},${totalComplexity},${maxScore},${maxLine}`;
});
fs.writeFileSync(outputPath, [header, ...rows].join('\n'), 'utf8');
}
function main() {
const projectRoot = process.cwd();
const tsxFiles = collectTsxFiles(projectRoot);
const results = tsxFiles
.map(getFileComplexity)
.filter((item) => item && item.totalComplexity > 0)
.sort((a, b) => b.maxComplexityInfo?.score - a.maxComplexityInfo?.score);
const outputPath = path.join(projectRoot, 'complexity-report.csv');
writeCsv(results, outputPath);
console.log(`CSV report written to ${outputPath}`);
}
main();

View File

@@ -179,6 +179,7 @@
"@types/semver": "^7.7.1",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^10.0.0",
"@typescript-eslint/parser": "^8.48.0",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"bing-translate-api": "^4.1.0",

107
web/pnpm-lock.yaml generated
View File

@@ -457,6 +457,9 @@ importers:
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
'@typescript-eslint/parser':
specifier: ^8.48.0
version: 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
autoprefixer:
specifier: ^10.4.21
version: 10.4.21(postcss@8.5.6)
@@ -3386,8 +3389,8 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.46.2':
resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==}
'@typescript-eslint/parser@8.48.0':
resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@@ -3399,16 +3402,32 @@ packages:
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.48.0':
resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.46.2':
resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/scope-manager@8.48.0':
resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.46.2':
resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/tsconfig-utils@8.48.0':
resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.46.2':
resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3420,12 +3439,22 @@ packages:
resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.48.0':
resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.46.2':
resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/typescript-estree@8.48.0':
resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.46.2':
resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -3437,6 +3466,10 @@ packages:
resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/visitor-keys@8.48.0':
resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -8503,8 +8536,8 @@ snapshots:
'@eslint-community/eslint-plugin-eslint-comments': 4.5.0(eslint@9.38.0(jiti@1.21.7))
'@eslint/markdown': 7.4.1
'@stylistic/eslint-plugin': 5.5.0(eslint@9.38.0(jiti@1.21.7))
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@vitest/eslint-plugin': 1.3.23(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
ansis: 4.2.0
cac: 6.7.14
@@ -8524,8 +8557,8 @@ snapshots:
eslint-plugin-regexp: 2.10.0(eslint@9.38.0(jiti@1.21.7))
eslint-plugin-toml: 0.12.0(eslint@9.38.0(jiti@1.21.7))
eslint-plugin-unicorn: 61.0.2(eslint@9.38.0(jiti@1.21.7))
eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))
eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@1.21.7)))(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@1.21.7)))
eslint-plugin-unused-imports: 4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))
eslint-plugin-vue: 10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@1.21.7)))(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@1.21.7)))
eslint-plugin-yml: 1.19.0(eslint@9.38.0(jiti@1.21.7))
eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.22)(eslint@9.38.0(jiti@1.21.7))
globals: 16.4.0
@@ -11831,10 +11864,10 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.46.2
'@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
@@ -11848,12 +11881,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
'@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.46.2
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.46.2
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
eslint: 9.38.0(jiti@1.21.7)
typescript: 5.9.3
@@ -11869,15 +11902,33 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.48.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
'@typescript-eslint/types': 8.48.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.46.2':
dependencies:
'@typescript-eslint/types': 8.46.2
'@typescript-eslint/visitor-keys': 8.46.2
'@typescript-eslint/scope-manager@8.48.0':
dependencies:
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/visitor-keys': 8.48.0
'@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.46.2
@@ -11892,6 +11943,8 @@ snapshots:
'@typescript-eslint/types@8.46.2': {}
'@typescript-eslint/types@8.48.0': {}
'@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.46.2(typescript@5.9.3)
@@ -11908,6 +11961,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.48.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@1.21.7))
@@ -11924,6 +11992,11 @@ snapshots:
'@typescript-eslint/types': 8.46.2
eslint-visitor-keys: 4.2.1
'@typescript-eslint/visitor-keys@8.48.0':
dependencies:
'@typescript-eslint/types': 8.48.0
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.3.0': {}
'@vitest/eslint-plugin@1.3.23(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)':
@@ -13651,13 +13724,13 @@ snapshots:
semver: 7.7.3
strip-indent: 4.1.1
eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7)):
eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7)):
dependencies:
eslint: 9.38.0(jiti@1.21.7)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@1.21.7)))(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@1.21.7))):
eslint-plugin-vue@10.5.1(@stylistic/eslint-plugin@5.5.0(eslint@9.38.0(jiti@1.21.7)))(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@1.21.7))):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@1.21.7))
eslint: 9.38.0(jiti@1.21.7)
@@ -13669,7 +13742,7 @@ snapshots:
xml-name-validator: 4.0.0
optionalDependencies:
'@stylistic/eslint-plugin': 5.5.0(eslint@9.38.0(jiti@1.21.7))
'@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)
eslint-plugin-yml@1.19.0(eslint@9.38.0(jiti@1.21.7)):
dependencies:

View File

@@ -1,14 +1,15 @@
import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WebhookTriggerResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app'
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
export const fetchAppList = ({ url, params }: { url: string; params?: Record<string, any> }): Promise<AppListResponse> => {
export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
return get<AppListResponse>(url, { params })
}
export const fetchAppDetail = ({ url, id }: { url: string; id: string }): Promise<AppDetailResponse> => {
export const fetchAppDetail: Fetcher<AppDetailResponse, { url: string; id: string }> = ({ url, id }) => {
return get<AppDetailResponse>(`${url}/${id}`)
}
@@ -17,74 +18,24 @@ export const fetchAppDetailDirect = async ({ url, id }: { url: string; id: strin
return get<AppDetailResponse>(`${url}/${id}`)
}
export const fetchAppTemplates = ({ url }: { url: string }): Promise<AppTemplatesResponse> => {
export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
return get<AppTemplatesResponse>(url)
}
export const createApp = ({
name,
icon_type,
icon,
icon_background,
mode,
description,
config,
}: {
name: string
icon_type?: AppIconType
icon?: string
icon_background?: string
mode: AppModeEnum
description?: string
config?: ModelConfig
}): Promise<AppDetailResponse> => {
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppModeEnum; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
}
export const updateAppInfo = ({
appID,
name,
icon_type,
icon,
icon_background,
description,
use_icon_as_answer_icon,
max_active_requests,
}: {
appID: string
name: string
icon_type: AppIconType
icon: string
icon_background?: string
description: string
use_icon_as_answer_icon?: boolean
max_active_requests?: number | null
}): Promise<AppDetailResponse> => {
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean; max_active_requests?: number | null }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }) => {
const body = { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests }
return put<AppDetailResponse>(`apps/${appID}`, { body })
}
export const copyApp = ({
appID,
name,
icon_type,
icon,
icon_background,
mode,
description,
}: {
appID: string
name: string
icon_type: AppIconType
icon: string
icon_background?: string | null
mode: AppModeEnum
description?: string
}): Promise<AppDetailResponse> => {
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppModeEnum; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
}
export const exportAppConfig = ({ appID, include = false, workflowID }: { appID: string; include?: boolean; workflowID?: string }): Promise<{ data: string }> => {
export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean; workflowID?: string }> = ({ appID, include = false, workflowID }) => {
const params = new URLSearchParams({
include_secret: include.toString(),
})
@@ -93,116 +44,126 @@ export const exportAppConfig = ({ appID, include = false, workflowID }: { appID:
return get<{ data: string }>(`apps/${appID}/export?${params.toString()}`)
}
export const importDSL = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }: { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }): Promise<DSLImportResponse> => {
// TODO: delete
export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } })
}
// TODO: delete
export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => {
return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } })
}
export const importDSL: Fetcher<DSLImportResponse, { mode: DSLImportMode; yaml_content?: string; yaml_url?: string; app_id?: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ mode, yaml_content, yaml_url, app_id, name, description, icon_type, icon, icon_background }) => {
return post<DSLImportResponse>('apps/imports', { body: { mode, yaml_content, yaml_url, app_id, name, description, icon, icon_type, icon_background } })
}
export const importDSLConfirm = ({ import_id }: { import_id: string }): Promise<DSLImportResponse> => {
export const importDSLConfirm: Fetcher<DSLImportResponse, { import_id: string }> = ({ import_id }) => {
return post<DSLImportResponse>(`apps/imports/${import_id}/confirm`, { body: {} })
}
export const switchApp = ({ appID, name, icon_type, icon, icon_background }: { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }): Promise<{ new_app_id: string }> => {
export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => {
return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } })
}
export const deleteApp = (appID: string): Promise<CommonResponse> => {
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
return del<CommonResponse>(`apps/${appID}`)
}
export const updateAppSiteStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
export const updateAppSiteStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppApiStatus = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
export const updateAppApiStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
// path: /apps/{appId}/rate-limit
export const updateAppRateLimit = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
export const updateAppRateLimit: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<AppDetailResponse>(url, { body })
}
export const updateAppSiteAccessToken = ({ url }: { url: string }): Promise<UpdateAppSiteCodeResponse> => {
export const updateAppSiteAccessToken: Fetcher<UpdateAppSiteCodeResponse, { url: string }> = ({ url }) => {
return post<UpdateAppSiteCodeResponse>(url)
}
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<AppDetailResponse> => {
export const updateAppSiteConfig = ({ url, body }: { url: string; body: Record<string, any> }) => {
return post<AppDetailResponse>(url, { body })
}
export const getAppDailyMessages = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyMessagesResponse> => {
export const getAppDailyMessages: Fetcher<AppDailyMessagesResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyMessagesResponse>(url, { params })
}
export const getAppDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyConversationsResponse> => {
export const getAppDailyConversations: Fetcher<AppDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyConversationsResponse>(url, { params })
}
export const getWorkflowDailyConversations = ({ url, params }: { url: string; params: Record<string, any> }): Promise<WorkflowDailyConversationsResponse> => {
export const getWorkflowDailyConversations: Fetcher<WorkflowDailyConversationsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<WorkflowDailyConversationsResponse>(url, { params })
}
export const getAppStatistics = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppStatisticsResponse> => {
export const getAppStatistics: Fetcher<AppStatisticsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppStatisticsResponse>(url, { params })
}
export const getAppDailyEndUsers = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppDailyEndUsersResponse> => {
export const getAppDailyEndUsers: Fetcher<AppDailyEndUsersResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppDailyEndUsersResponse>(url, { params })
}
export const getAppTokenCosts = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppTokenCostsResponse> => {
export const getAppTokenCosts: Fetcher<AppTokenCostsResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppTokenCostsResponse>(url, { params })
}
export const updateAppModelConfig = ({ url, body }: { url: string; body: Record<string, any> }): Promise<UpdateAppModelConfigResponse> => {
export const updateAppModelConfig: Fetcher<UpdateAppModelConfigResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<UpdateAppModelConfigResponse>(url, { body })
}
// For temp testing
export const fetchAppListNoMock = ({ url, params }: { url: string; params: Record<string, any> }): Promise<AppListResponse> => {
export const fetchAppListNoMock: Fetcher<AppListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<AppListResponse>(url, params)
}
export const fetchApiKeysList = ({ url, params }: { url: string; params: Record<string, any> }): Promise<ApiKeysListResponse> => {
export const fetchApiKeysList: Fetcher<ApiKeysListResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<ApiKeysListResponse>(url, params)
}
export const delApikey = ({ url, params }: { url: string; params: Record<string, any> }): Promise<CommonResponse> => {
export const delApikey: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return del<CommonResponse>(url, params)
}
export const createApikey = ({ url, body }: { url: string; body: Record<string, any> }): Promise<CreateApiKeyResponse> => {
export const createApikey: Fetcher<CreateApiKeyResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
return post<CreateApiKeyResponse>(url, body)
}
export const validateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<ValidateOpenAIKeyResponse> => {
export const validateOpenAIKey: Fetcher<ValidateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post<ValidateOpenAIKeyResponse>(url, { body })
}
export const updateOpenAIKey = ({ url, body }: { url: string; body: { token: string } }): Promise<UpdateOpenAIKeyResponse> => {
export const updateOpenAIKey: Fetcher<UpdateOpenAIKeyResponse, { url: string; body: { token: string } }> = ({ url, body }) => {
return post<UpdateOpenAIKeyResponse>(url, { body })
}
export const generationIntroduction = ({ url, body }: { url: string; body: { prompt_template: string } }): Promise<GenerationIntroductionResponse> => {
export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { url: string; body: { prompt_template: string } }> = ({ url, body }) => {
return post<GenerationIntroductionResponse>(url, { body })
}
export const fetchAppVoices = ({ appId, language }: { appId: string; language?: string }): Promise<AppVoicesListResponse> => {
export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => {
language = language || 'en-US'
return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`)
}
// Tracing
export const fetchTracingStatus = ({ appId }: { appId: string }): Promise<TracingStatus> => {
return get<TracingStatus>(`/apps/${appId}/trace`)
export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => {
return get(`/apps/${appId}/trace`)
}
export const updateTracingStatus = ({ appId, body }: { appId: string; body: Record<string, any> }): Promise<CommonResponse> => {
return post<CommonResponse>(`/apps/${appId}/trace`, { body })
export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace`, { body })
}
// Webhook Trigger
export const fetchWebhookUrl = ({ appId, nodeId }: { appId: string; nodeId: string }): Promise<WebhookTriggerResponse> => {
export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; nodeId: string }> = ({ appId, nodeId }) => {
return get<WebhookTriggerResponse>(
`apps/${appId}/workflows/triggers/webhook`,
{ params: { node_id: nodeId } },
@@ -210,22 +171,22 @@ export const fetchWebhookUrl = ({ appId, nodeId }: { appId: string; nodeId: stri
)
}
export const fetchTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<TracingConfig & { has_not_configured: true }> => {
return get<TracingConfig & { has_not_configured: true }>(`/apps/${appId}/trace-config`, {
export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return get(`/apps/${appId}/trace-config`, {
params: {
tracing_provider: provider,
},
})
}
export const addTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
return post<CommonResponse>(`/apps/${appId}/trace-config`, { body })
export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return post(`/apps/${appId}/trace-config`, { body })
}
export const updateTracingConfig = ({ appId, body }: { appId: string; body: TracingConfig }): Promise<CommonResponse> => {
return patch<CommonResponse>(`/apps/${appId}/trace-config`, { body })
export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
return patch(`/apps/${appId}/trace-config`, { body })
}
export const removeTracingConfig = ({ appId, provider }: { appId: string; provider: TracingProvider }): Promise<CommonResponse> => {
return del<CommonResponse>(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
}

View File

@@ -1,85 +1,38 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { createApp, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import useSWR, { useSWRConfig } from 'swr'
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import Loading from '@/app/components/base/loading'
import { AppModeEnum } from '@/types/app'
import {
useAppDailyConversations,
useAppDailyEndUsers,
useAppDetail,
useAppList,
} from '../use-apps'
const Service: FC = () => {
const appId = '1'
const queryClient = useQueryClient()
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
const { data: updateAppSiteStatusRes, error: err1 } = useSWR({ url: '/apps', id: '1', body: { enable_site: false } }, updateAppSiteStatus)
const { data: updateAppApiStatusRes, error: err2 } = useSWR({ url: '/apps', id: '1', body: { enable_api: true } }, updateAppApiStatus)
const { data: updateAppRateLimitRes, error: err3 } = useSWR({ url: '/apps', id: '1', body: { api_rpm: 10, api_rph: 20 } }, updateAppRateLimit)
const { data: updateAppSiteCodeRes, error: err4 } = useSWR({ url: '/apps', id: '1', body: {} }, updateAppSiteAccessToken)
const { data: updateAppSiteConfigRes, error: err5 } = useSWR({ url: '/apps', id: '1', body: { title: 'title test', author: 'author test' } }, updateAppSiteConfig)
const { data: getAppDailyConversationsRes, error: err6 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyConversations)
const { data: getAppDailyEndUsersRes, error: err7 } = useSWR({ url: '/apps', id: '1', body: { start: '1', end: '2' } }, getAppDailyEndUsers)
const { data: updateAppModelConfigRes, error: err8 } = useSWR({ url: '/apps', id: '1', body: { model_id: 'gpt-100' } }, updateAppModelConfig)
const { data: appList, error: appListError, isLoading: isAppListLoading } = useAppList({ page: 1, limit: 30, name: '' })
const { data: firstApp, error: appDetailError, isLoading: isAppDetailLoading } = useAppDetail(appId)
const { data: updateAppSiteStatusRes, error: err1, isLoading: isUpdatingSiteStatus } = useQuery({
queryKey: ['demo', 'updateAppSiteStatus', appId],
queryFn: () => updateAppSiteStatus({ url: '/apps', body: { enable_site: false } }),
})
const { data: updateAppApiStatusRes, error: err2, isLoading: isUpdatingApiStatus } = useQuery({
queryKey: ['demo', 'updateAppApiStatus', appId],
queryFn: () => updateAppApiStatus({ url: '/apps', body: { enable_api: true } }),
})
const { data: updateAppRateLimitRes, error: err3, isLoading: isUpdatingRateLimit } = useQuery({
queryKey: ['demo', 'updateAppRateLimit', appId],
queryFn: () => updateAppRateLimit({ url: '/apps', body: { api_rpm: 10, api_rph: 20 } }),
})
const { data: updateAppSiteCodeRes, error: err4, isLoading: isUpdatingSiteCode } = useQuery({
queryKey: ['demo', 'updateAppSiteAccessToken', appId],
queryFn: () => updateAppSiteAccessToken({ url: '/apps' }),
})
const { data: updateAppSiteConfigRes, error: err5, isLoading: isUpdatingSiteConfig } = useQuery({
queryKey: ['demo', 'updateAppSiteConfig', appId],
queryFn: () => updateAppSiteConfig({ url: '/apps', body: { title: 'title test', author: 'author test' } }),
})
const { data: getAppDailyConversationsRes, error: err6, isLoading: isConversationsLoading } = useAppDailyConversations(appId, { start: '1', end: '2' })
const { data: getAppDailyEndUsersRes, error: err7, isLoading: isEndUsersLoading } = useAppDailyEndUsers(appId, { start: '1', end: '2' })
const { data: updateAppModelConfigRes, error: err8, isLoading: isUpdatingModelConfig } = useQuery({
queryKey: ['demo', 'updateAppModelConfig', appId],
queryFn: () => updateAppModelConfig({ url: '/apps', body: { model_id: 'gpt-100' } }),
})
const { mutateAsync: mutateCreateApp } = useMutation({
mutationKey: ['demo', 'createApp'],
mutationFn: () => createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: AppModeEnum.CHAT,
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['apps', 'list'],
})
},
})
const { mutate } = useSWRConfig()
const handleCreateApp = async () => {
await mutateCreateApp()
await createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: AppModeEnum.CHAT,
})
// reload app list
mutate({ url: '/apps', params: { page: 1 } })
}
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
return <div>{JSON.stringify(appListError ?? appDetailError ?? err1 ?? err2 ?? err3 ?? err4 ?? err5 ?? err6 ?? err7 ?? err8)}</div>
return <div>{JSON.stringify(appListError)}</div>
const isLoading = isAppListLoading
|| isAppDetailLoading
|| isUpdatingSiteStatus
|| isUpdatingApiStatus
|| isUpdatingRateLimit
|| isUpdatingSiteCode
|| isUpdatingSiteConfig
|| isConversationsLoading
|| isEndUsersLoading
|| isUpdatingModelConfig
if (isLoading || !appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
if (!appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
return <Loading />
return (

View File

@@ -1,63 +1,31 @@
import { get, post } from './base'
import type {
ApiKeysListResponse,
AppDailyConversationsResponse,
AppDailyEndUsersResponse,
AppDailyMessagesResponse,
AppListResponse,
AppStatisticsResponse,
AppTokenCostsResponse,
AppVoicesListResponse,
WorkflowDailyConversationsResponse,
} from '@/models/app'
import type { App, AppModeEnum } from '@/types/app'
import type { App } from '@/types/app'
import type { AppListResponse } from '@/models/app'
import { useInvalid } from './use-base'
import {
useInfiniteQuery,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { useQuery } from '@tanstack/react-query'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
const NAME_SPACE = 'apps'
type AppListParams = {
page?: number
limit?: number
name?: string
mode?: AppModeEnum | 'all'
tag_ids?: string[]
is_created_by_me?: boolean
}
type DateRangeParams = {
start?: string
end?: string
}
const normalizeAppListParams = (params: AppListParams) => {
const {
page = 1,
limit = 30,
name = '',
mode,
tag_ids,
is_created_by_me,
} = params
return {
page,
limit,
name,
...(mode && mode !== 'all' ? { mode } : {}),
...(tag_ids?.length ? { tag_ids } : {}),
...(is_created_by_me ? { is_created_by_me } : {}),
}
}
const appListKey = (params: AppListParams) => [NAME_SPACE, 'list', params]
// TODO paging for list
const useAppFullListKey = [NAME_SPACE, 'full-list']
export const useAppFullList = () => {
return useQuery<AppListResponse>({
queryKey: useAppFullListKey,
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100 } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
})
}
export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => {
return useQuery({
@@ -71,142 +39,3 @@ export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean)
retry: 0,
})
}
export const useAppDetail = (appID: string) => {
return useQuery<App>({
queryKey: [NAME_SPACE, 'detail', appID],
queryFn: () => get<App>(`/apps/${appID}`),
enabled: !!appID,
})
}
export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
const normalizedParams = normalizeAppListParams(params)
return useQuery<AppListResponse>({
queryKey: appListKey(normalizedParams),
queryFn: () => get<AppListResponse>('/apps', { params: normalizedParams }),
...options,
})
}
export const useAppFullList = () => {
return useQuery<AppListResponse>({
queryKey: useAppFullListKey,
queryFn: () => get<AppListResponse>('/apps', { params: { page: 1, limit: 100, name: '' } }),
})
}
export const useInvalidateAppFullList = () => {
return useInvalid(useAppFullListKey)
}
export const useInfiniteAppList = (params: AppListParams, options?: { enabled?: boolean }) => {
const normalizedParams = normalizeAppListParams(params)
return useInfiniteQuery<AppListResponse>({
queryKey: appListKey(normalizedParams),
queryFn: ({ pageParam = normalizedParams.page }) => get<AppListResponse>('/apps', { params: { ...normalizedParams, page: pageParam } }),
getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined,
initialPageParam: normalizedParams.page,
...options,
})
}
export const useInvalidateAppList = () => {
const queryClient = useQueryClient()
return () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'list'],
})
}
}
const useAppStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
return useQuery<T>({
queryKey: [NAME_SPACE, 'statistics', metric, appId, params],
queryFn: () => get<T>(`/apps/${appId}/statistics/${metric}`, { params }),
enabled: !!appId,
})
}
const useWorkflowStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
return useQuery<T>({
queryKey: [NAME_SPACE, 'workflow-statistics', metric, appId, params],
queryFn: () => get<T>(`/apps/${appId}/workflow/statistics/${metric}`, { params }),
enabled: !!appId,
})
}
export const useAppDailyMessages = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyMessagesResponse>('daily-messages', appId, params)
}
export const useAppDailyConversations = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyConversationsResponse>('daily-conversations', appId, params)
}
export const useAppDailyEndUsers = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppDailyEndUsersResponse>('daily-end-users', appId, params)
}
export const useAppAverageSessionInteractions = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('average-session-interactions', appId, params)
}
export const useAppAverageResponseTime = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('average-response-time', appId, params)
}
export const useAppTokensPerSecond = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('tokens-per-second', appId, params)
}
export const useAppSatisfactionRate = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppStatisticsResponse>('user-satisfaction-rate', appId, params)
}
export const useAppTokenCosts = (appId: string, params?: DateRangeParams) => {
return useAppStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
}
export const useWorkflowDailyConversations = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<WorkflowDailyConversationsResponse>('daily-conversations', appId, params)
}
export const useWorkflowDailyTerminals = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppDailyEndUsersResponse>('daily-terminals', appId, params)
}
export const useWorkflowTokenCosts = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppTokenCostsResponse>('token-costs', appId, params)
}
export const useWorkflowAverageInteractions = (appId: string, params?: DateRangeParams) => {
return useWorkflowStatisticsQuery<AppStatisticsResponse>('average-app-interactions', appId, params)
}
export const useAppVoices = (appId?: string, language?: string) => {
return useQuery<AppVoicesListResponse>({
queryKey: [NAME_SPACE, 'voices', appId, language || 'en-US'],
queryFn: () => get<AppVoicesListResponse>(`/apps/${appId}/text-to-audio/voices`, { params: { language: language || 'en-US' } }),
enabled: !!appId,
})
}
export const useAppApiKeys = (appId?: string, options?: { enabled?: boolean }) => {
return useQuery<ApiKeysListResponse>({
queryKey: [NAME_SPACE, 'api-keys', appId],
queryFn: () => get<ApiKeysListResponse>(`/apps/${appId}/api-keys`),
enabled: !!appId && (options?.enabled ?? true),
})
}
export const useInvalidateAppApiKeys = () => {
const queryClient = useQueryClient()
return (appId?: string) => {
if (!appId)
return
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'api-keys', appId],
})
}
}