From 6c5d41135ceade0d0e6d56b187bb29a09b8bf5a4 Mon Sep 17 00:00:00 2001 From: ade <2329571595@qq.com> Date: Wed, 18 Sep 2024 23:49:43 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E4=B8=AD=E5=BF=83=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/center/AddAgentType.tsx | 96 ++++ .../(commonLayout)/agent/center/AppCard.tsx | 422 ++++++++++++++++++ web/app/(commonLayout)/agent/center/Apps.tsx | 332 ++++++++++++++ .../agent/center/DraggableList.tsx | 108 +++++ .../agent/center/EditAgentType.tsx | 105 +++++ .../agent/center/NewAppCard.tsx | 101 +++++ .../agent/center/assets/add.svg | 3 + .../agent/center/assets/chat-solid.svg | 4 + .../agent/center/assets/chat.svg | 3 + .../agent/center/assets/completion-solid.svg | 4 + .../agent/center/assets/completion.svg | 3 + .../agent/center/assets/discord.svg | 3 + .../agent/center/assets/github.svg | 17 + .../agent/center/assets/link-gray.svg | 3 + .../agent/center/assets/link.svg | 3 + .../agent/center/assets/right-arrow.svg | 3 + .../agent/center/hooks/useAppsQueryState.ts | 53 +++ web/app/(commonLayout)/agent/center/page.tsx | 25 ++ .../agent/center/style.module.css | 29 ++ .../components/app/app-publisher/index.tsx | 77 +++- web/app/components/header/agent-nav/index.tsx | 37 ++ web/app/components/header/index.tsx | 3 + web/app/components/workflow/header/index.tsx | 54 ++- web/config/index.ts | 2 +- web/i18n/zh-Hans/common.ts | 1 + web/i18n/zh-Hans/workflow.ts | 1 + web/service/agent.ts | 73 +++ web/service/base.ts | 23 +- 28 files changed, 1557 insertions(+), 31 deletions(-) create mode 100644 web/app/(commonLayout)/agent/center/AddAgentType.tsx create mode 100644 web/app/(commonLayout)/agent/center/AppCard.tsx create mode 100644 web/app/(commonLayout)/agent/center/Apps.tsx create mode 100644 web/app/(commonLayout)/agent/center/DraggableList.tsx create mode 100644 web/app/(commonLayout)/agent/center/EditAgentType.tsx create mode 100644 web/app/(commonLayout)/agent/center/NewAppCard.tsx create mode 100644 web/app/(commonLayout)/agent/center/assets/add.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/chat-solid.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/chat.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/completion-solid.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/completion.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/discord.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/github.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/link-gray.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/link.svg create mode 100644 web/app/(commonLayout)/agent/center/assets/right-arrow.svg create mode 100644 web/app/(commonLayout)/agent/center/hooks/useAppsQueryState.ts create mode 100644 web/app/(commonLayout)/agent/center/page.tsx create mode 100644 web/app/(commonLayout)/agent/center/style.module.css create mode 100644 web/app/components/header/agent-nav/index.tsx create mode 100644 web/service/agent.ts diff --git a/web/app/(commonLayout)/agent/center/AddAgentType.tsx b/web/app/(commonLayout)/agent/center/AddAgentType.tsx new file mode 100644 index 000000000..57285431e --- /dev/null +++ b/web/app/(commonLayout)/agent/center/AddAgentType.tsx @@ -0,0 +1,96 @@ +'use client' +import React, { useState } from 'react' +import s from './style.module.css' +import cn from '@/utils/classnames' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { useProviderContext } from '@/context/provider-context' +import { agentTypeAdd } from '@/service/agent' + +export type DuplicateAppModalProps = { + show: boolean + onHide: () => void +} + +const AddAgentType = ({ + show = false, + onHide, +}: DuplicateAppModalProps) => { + const [name, setName] = React.useState('') + const [description, setDescription] = useState('') + const [sort, setSort] = useState() + const { plan, enableBilling } = useProviderContext() + const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) + + const submit = () => { + if (!name.trim()) { + Toast.notify({ type: 'error', message: '请输入类型名称' }) + return + } + agentTypeAdd('/dify/agent-type/add', { + name, + description, + sort, + }).then((r) => { + if (r.data) { + Toast.notify({ type: 'success', message: '添加成功' }) + onHide() + } + else { + Toast.notify({ type: 'error', message: r.message }) + } + }) + } + return ( + <> + { + }} + className={cn(s.modal, '!max-w-[480px]', 'px-8')} + > + +

添加智能体分类

+
+
分类名称
+
+ setName(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+
分类描述
+
+ setDescription(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+
排序
+
+ setSort(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+ + +
+
+ + + ) +} + +export default AddAgentType diff --git a/web/app/(commonLayout)/agent/center/AppCard.tsx b/web/app/(commonLayout)/agent/center/AppCard.tsx new file mode 100644 index 000000000..fb39dee5a --- /dev/null +++ b/web/app/(commonLayout)/agent/center/AppCard.tsx @@ -0,0 +1,422 @@ +'use client' + +import { useContext, useContextSelector } from 'use-context-selector' +import { useRouter } from 'next/navigation' +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiMoreFill } from '@remixicon/react' +import s from './style.module.css' +import cn from '@/utils/classnames' +import type { App } from '@/types/app' +import Confirm from '@/app/components/base/confirm' +import { ToastContext } from '@/app/components/base/toast' +import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' +import DuplicateAppModal from '@/app/components/app/duplicate-modal' +import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' +import AppIcon from '@/app/components/base/app-icon' +import AppsContext, { useAppContext } from '@/context/app-context' +import type { HtmlContentProps } from '@/app/components/base/popover' +import CustomPopover from '@/app/components/base/popover' +import Divider from '@/app/components/base/divider' +import { getRedirection } from '@/utils/app-redirection' +import { useProviderContext } from '@/context/provider-context' +import { NEED_REFRESH_APP_LIST_KEY } from '@/config' +import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication' +import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' +import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' +import EditAppModal from '@/app/components/explore/create-app-modal' +import SwitchAppModal from '@/app/components/app/switch-app-modal' +import type { Tag } from '@/app/components/base/tag-management/constant' +import TagSelector from '@/app/components/base/tag-management/selector' +import type { EnvironmentVariable } from '@/app/components/workflow/types' +import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' +import { fetchWorkflowDraft } from '@/service/workflow' + +export type AppCardProps = { + app: App + onRefresh?: () => void +} + +const AppCard = ({ app, onRefresh }: AppCardProps) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const { isCurrentWorkspaceEditor } = useAppContext() + const { onPlanInfoChanged } = useProviderContext() + const { push } = useRouter() + + const mutateApps = useContextSelector( + AppsContext, + state => state.mutateApps, + ) + + const [showEditModal, setShowEditModal] = useState(false) + const [showDuplicateModal, setShowDuplicateModal] = useState(false) + const [showSwitchModal, setShowSwitchModal] = useState(false) + const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [secretEnvList, setSecretEnvList] = useState([]) + + const onConfirmDelete = useCallback(async () => { + try { + await deleteApp(app.id) + notify({ type: 'success', message: t('app.appDeleted') }) + if (onRefresh) + onRefresh() + mutateApps() + onPlanInfoChanged() + } + catch (e: any) { + notify({ + type: 'error', + message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`, + }) + } + setShowConfirmDelete(false) + }, [app.id]) + + const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ + name, + icon_type, + icon, + icon_background, + description, + use_icon_as_answer_icon, + }) => { + try { + await updateAppInfo({ + appID: app.id, + name, + icon_type, + icon, + icon_background, + description, + use_icon_as_answer_icon, + }) + setShowEditModal(false) + notify({ + type: 'success', + message: t('app.editDone'), + }) + if (onRefresh) + onRefresh() + mutateApps() + } + catch (e) { + notify({ type: 'error', message: t('app.editFailed') }) + } + }, [app.id, mutateApps, notify, onRefresh, t]) + + const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { + try { + const newApp = await copyApp({ + appID: app.id, + name, + icon_type, + icon, + icon_background, + mode: app.mode, + }) + setShowDuplicateModal(false) + notify({ + type: 'success', + message: t('app.newApp.appCreated'), + }) + localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') + if (onRefresh) + onRefresh() + mutateApps() + onPlanInfoChanged() + getRedirection(isCurrentWorkspaceEditor, newApp, push) + } + catch (e) { + notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + } + + const onExport = async (include = false) => { + try { + const { data } = await exportAppConfig({ + appID: app.id, + include, + }) + const a = document.createElement('a') + const file = new Blob([data], { type: 'application/yaml' }) + a.href = URL.createObjectURL(file) + a.download = `${app.name}.yml` + a.click() + } + catch (e) { + notify({ type: 'error', message: t('app.exportFailed') }) + } + } + + const exportCheck = async () => { + if (app.mode !== 'workflow' && app.mode !== 'advanced-chat') { + onExport() + return + } + try { + const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`) + const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') + if (list.length === 0) { + onExport() + return + } + setSecretEnvList(list) + } + catch (e) { + notify({ type: 'error', message: t('app.exportFailed') }) + } + } + + const onSwitch = () => { + if (onRefresh) + onRefresh() + mutateApps() + setShowSwitchModal(false) + } + + const Operations = (props: HtmlContentProps) => { + const onMouseLeave = async () => { + props.onClose?.() + } + const onClickSettings = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowEditModal(true) + } + const onClickDuplicate = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowDuplicateModal(true) + } + const onClickExport = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + exportCheck() + } + const onClickSwitch = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowSwitchModal(true) + } + const onClickDelete = async (e: React.MouseEvent) => { + e.stopPropagation() + props.onClick?.() + e.preventDefault() + setShowConfirmDelete(true) + } + return ( +
+ + + + + {(app.mode === 'completion' || app.mode === 'chat') && ( + <> + +
+ {t('app.switch')} +
+ + )} + +
+ + {t('common.operation.delete')} + +
+
+ ) + } + + const [tags, setTags] = useState(app.tags) + useEffect(() => { + setTags(app.tags) + }, [app.tags]) + + return ( + <> +
{ + e.preventDefault() + getRedirection(isCurrentWorkspaceEditor, app, push) + }} + className='relative group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' + > +
+
+ + + {app.mode === 'advanced-chat' && ( + + )} + {app.mode === 'agent-chat' && ( + + )} + {app.mode === 'chat' && ( + + )} + {app.mode === 'completion' && ( + + )} + {app.mode === 'workflow' && ( + + )} + +
+
+
+
{app.name}
+
+
+ {app.mode === 'advanced-chat' &&
{t('app.types.chatbot').toUpperCase()}
} + {app.mode === 'chat' &&
{t('app.types.chatbot').toUpperCase()}
} + {app.mode === 'agent-chat' &&
{t('app.types.agent').toUpperCase()}
} + {app.mode === 'workflow' &&
{t('app.types.workflow').toUpperCase()}
} + {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
} +
+
+
+
+
+ {app.description} +
+
+
+ {isCurrentWorkspaceEditor && ( + <> +
{ + e.stopPropagation() + e.preventDefault() + }}> +
+ tag.id)} + selectedTags={tags} + onCacheUpdate={setTags} + onChange={onRefresh} + /> +
+
+
+
+ } + position="br" + trigger="click" + btnElement={ +
+ +
+ } + btnClassName={open => + cn( + open ? '!bg-black/5 !shadow-none' : '!bg-transparent', + 'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5', + ) + } + popupClassName={ + (app.mode === 'completion' || app.mode === 'chat') + ? '!w-[238px] translate-x-[-110px]' + : '' + } + className={'!w-[128px] h-fit !z-20'} + /> +
+ + )} +
+
+ {showEditModal && ( + setShowEditModal(false)} + /> + )} + {showDuplicateModal && ( + setShowDuplicateModal(false)} + /> + )} + {showSwitchModal && ( + setShowSwitchModal(false)} + onSuccess={onSwitch} + /> + )} + {showConfirmDelete && ( + setShowConfirmDelete(false)} + /> + )} + {secretEnvList.length > 0 && ( + setSecretEnvList([])} + /> + )} + + ) +} + +export default AppCard diff --git a/web/app/(commonLayout)/agent/center/Apps.tsx b/web/app/(commonLayout)/agent/center/Apps.tsx new file mode 100644 index 000000000..f66d57ec3 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/Apps.tsx @@ -0,0 +1,332 @@ +'use client' + +import React, { useCallback, useEffect, useRef, useState } from 'react' +import useSWRInfinite from 'swr/infinite' +import { useTranslation } from 'react-i18next' +import { useDebounceFn } from 'ahooks' + +import { RiRobot2Line, RiRobot3Line } from '@remixicon/react' +import cn from 'classnames' +import { Pagination } from 'react-headless-pagination' +import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' +import Image from 'next/image' +import useAppsQueryState from './hooks/useAppsQueryState' +import { APP_PAGE_LIMIT, NEED_REFRESH_APP_LIST_KEY } from '@/config' +import { CheckModal } from '@/hooks/use-pay' +import TabSliderNew from '@/app/components/base/tab-slider-new' +import { useTabSearchParams } from '@/hooks/use-tab-searchparams' +import SearchInput from '@/app/components/base/search-input' +import { agentTypeDelete, getAgentTypeList, getDifyList } from '@/service/agent' +import Button from '@/app/components/base/button' +import s from '@/app/components/app/annotation/style.module.css' +import AddAgentType from '@/app/(commonLayout)/agent/center/AddAgentType' +import Toast from '@/app/components/base/toast' +import EditAgentType from '@/app/(commonLayout)/agent/center/EditAgentType' +import DragDropSort from '@/app/(commonLayout)/agent/center/DraggableList' +import Confirm from '@/app/components/base/confirm' + +const getKey = ( + activeTab: string, +) => { + const params: any = { url: '/dify/list', params: { agentTypeId: activeTab } } + return params +} + +const Apps = () => { + const { t } = useTranslation() + const [activeTab, setActiveTab] = useTabSearchParams({ + defaultTab: 'all', + }) + const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState() + const [searchKeywords, setSearchKeywords] = useState(keywords) + const setKeywords = useCallback((keywords: string) => { + setQuery(prev => ({ ...prev, keywords })) + }, [setQuery]) + const [activeIndex, setActiveIndex] = useState(0) + const { mutate } = useSWRInfinite( + () => getKey(activeTab), + null, + { revalidateFirstPage: true }, + ) + + const anchorRef = useRef(null) + const [options, setOptions] = useState([]) + const [types, setTypes] = useState([]) + const [difysCurrPage, setDifysCurrPage] = React.useState(0) + const [currPage, setCurrPage] = React.useState(0) + const [total, setTotal] = useState(0) + const [showAddAgentModal, setShowAddAgentModal] = useState(false) + const [showEditAgentModal, setShowEditAgentModal] = useState(false) + const [showDrag, setShowDrag] = useState(false) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [deleteId, setDeleteId] = useState('') + const getTypes = () => { + getAgentTypeList('/dify/agent-type/list').then((res: any) => { + setOptions(res.data.map((item: any) => { + return { + value: item.id, + text: item.name, + } + })) + if (res.data.length > 0 && activeTab === 'all') + setActiveTab(res.data[0].id) + }) + } + const getOptTypes = () => { + getDifyList('/dify/agent-type/list', { page: currPage + 1, pageSize: APP_PAGE_LIMIT }).then((res: any) => { + setTotal(res.totalCount) + setTypes(res.data) + }) + } + const [difys, setDifys] = useState([]) + const [difysTotal, setDifysTotal] = useState(0) + const [row, setRow] = useState() + const getDifys = () => { + getDifyList('/dify/list', { name: searchKeywords, agentTypeId: activeTab, page: difysCurrPage + 1, pageSize: APP_PAGE_LIMIT }).then((res) => { + setDifys(res.data) + setDifysTotal(res.totalCount) + }) + } + + useEffect(() => { + document.title = '智能体中心 - Dify' + if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { + localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) + mutate() + } + getTypes() + getOptTypes() + getDifys() + }, [mutate, t]) + + const { run: handleSearch } = useDebounceFn(() => { + setSearchKeywords(keywords) + }, { wait: 500 }) + const handleKeywordsChange = (value: string) => { + setKeywords(value) + handleSearch() + } + useEffect(() => { + getTypes() + getOptTypes() + }, [currPage]) + + useEffect(() => { + getDifys() + }, [difysCurrPage, searchKeywords]) + return ( + <> +
+
{ + setActiveIndex(0) + }} className={cn('rounded-lg px-3 py-[7px] mr-4 flex items-center cursor-pointer hover:bg-gray-200', activeIndex === 0 && 'bg-white border-gray-200 shadow-xs text-primary-600 hover:bg-white')}> 智能体
+
{ + setActiveIndex(1) + }} className={cn('rounded-lg px-3 py-[7px] flex items-center cursor-pointer hover:bg-gray-200', activeIndex === 1 && 'bg-white border-gray-200 shadow-xs text-primary-600 hover:bg-white')}> 分类
+
+ { + activeIndex === 0 + ? ( +
+
+ +
+ +
+
+
+ +
+ +
+ ) + : (
+ + + + + + + + + + + + + + { + types.map((item: any) => { + return ( + + + + + + + + + ) + }) + } + +
类型名称描述排序创建时间更新时间操作
{item.name} + {item.description} + {item.sort} + {item.createdAt} + {item.updatedAt} + + + +
+ + + + {t('appLog.table.pagination.previous')} + +
+ +
+ + {t('appLog.table.pagination.next')} + + +
+
) + } + +
+ setShowDeleteModal(false)} + onConfirm={() => { + agentTypeDelete('/dify/agent-type/delete', { + id: deleteId, + }).then((r) => { + if (r.data) { + Toast.notify({ type: 'success', message: '删除成功' }) + getOptTypes() + } + }) + setShowDeleteModal(false) + }} + title="确定要删除吗?" + /> + {showAddAgentModal && ( + { + setShowAddAgentModal(false) + getOptTypes() + }}/> + )} + {showEditAgentModal && ( + { + setShowEditAgentModal(false) + getTypes() + }}/> + )} + { + showDrag && ( + { + setShowDrag(false) + getDifys() + }}/> + ) + } + + ) +} + +export default Apps diff --git a/web/app/(commonLayout)/agent/center/DraggableList.tsx b/web/app/(commonLayout)/agent/center/DraggableList.tsx new file mode 100644 index 000000000..20402c5ec --- /dev/null +++ b/web/app/(commonLayout)/agent/center/DraggableList.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import cn from 'classnames' +import Button from '@/app/components/base/button' +import Modal from '@/app/components/base/modal' +import { agentUpdateSortBatch, getDifyList } from '@/service/agent' +import Toast from '@/app/components/base/toast' + +type Item = { + appId: number + name: string + agentSort: number +} + +export type DuplicateAppModalProps = { + show: boolean + onHide: () => void + activeTab: string +} + +const DragDropSort: React.FC = ({ show = false, onHide, activeTab }) => { + const [items, setItems] = useState([]) + const [draggedIndex, setDraggedIndex] = useState(null) + + const handleDragStart = (e: React.DragEvent, index: number) => { + e.dataTransfer.setData('text/plain', index.toString()) + setDraggedIndex(index) + } + + const getDifys = () => { + getDifyList('/dify/list', { agentTypeId: activeTab, page: 1, pageSize: 999999 }).then((res) => { + setItems(res.data) + }) + } + + useEffect(() => { + if (show) + getDifys() + }, [show]) + + const handleDragOver = (index: number) => { + if (draggedIndex === null || draggedIndex === index) + return + + const newItems = [...items] + const draggedItem = newItems[draggedIndex] + newItems.splice(draggedIndex, 1) + newItems.splice(index, 0, draggedItem) + setItems(newItems) + setDraggedIndex(index) + } + + const handleDragEnd = () => { + setDraggedIndex(null) + } + + const submit = () => { + console.log(items) + const arr = items.map((item, index) => { + return { + appId: item.appId, + sort: index, + } + }) + agentUpdateSortBatch('/dify/agent/update-sort-batch', arr).then((r) => { + if (r.data) { + Toast.notify({ type: 'success', message: '操作成功' }) + onHide() + } + else { + Toast.notify({ type: 'error', message: r.message }) + } + }) + } + + return ( + +
+ +
+
+ {items.map((item, index) => ( +
handleDragStart(e, index)} + onDragOver={() => handleDragOver(index)} + onDragEnd={handleDragEnd} + > +
{item.name}
+
{item.agentSort}
+
+ ))} +
+
+ +
+
+ ) +} + +export default DragDropSort diff --git a/web/app/(commonLayout)/agent/center/EditAgentType.tsx b/web/app/(commonLayout)/agent/center/EditAgentType.tsx new file mode 100644 index 000000000..d2fc8d780 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/EditAgentType.tsx @@ -0,0 +1,105 @@ +'use client' +import React, { useState } from 'react' +import s from './style.module.css' +import cn from '@/utils/classnames' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { useProviderContext } from '@/context/provider-context' +import { agentTypeAdd } from '@/service/agent' + +export type DuplicateAppModalProps = { + show: boolean + onHide: () => void + row: { + name: string + description: string + sort: number + id: string + } +} + +const EditAgentType = ({ + show = false, + onHide, + row, +}: DuplicateAppModalProps) => { + const [name, setName] = React.useState(row.name) + const [description, setDescription] = useState(row.description) + const [sort, setSort] = useState(row.sort) + const { plan, enableBilling } = useProviderContext() + const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) + + const submit = () => { + if (!name.trim()) { + Toast.notify({ type: 'error', message: '请输入类型名称' }) + return + } + agentTypeAdd('/dify/agent-type/update', { + id: row.id, + name, + description, + sort, + }).then((r) => { + if (r.data) { + Toast.notify({ type: 'success', message: '更新成功' }) + onHide() + } + else { + Toast.notify({ type: 'error', message: r.message }) + } + }) + } + + return ( + <> + { + }} + className={cn(s.modal, '!max-w-[480px]', 'px-8')} + > + +

添加智能体分类

+
+
分类名称
+
+ setName(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+
分类描述
+
+ setDescription(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+
排序
+
+ setSort(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+ + +
+
+ + + ) +} + +export default EditAgentType diff --git a/web/app/(commonLayout)/agent/center/NewAppCard.tsx b/web/app/(commonLayout)/agent/center/NewAppCard.tsx new file mode 100644 index 000000000..c0dffa99a --- /dev/null +++ b/web/app/(commonLayout)/agent/center/NewAppCard.tsx @@ -0,0 +1,101 @@ +'use client' + +import { forwardRef, useMemo, useState } from 'react' +import { + useRouter, + useSearchParams, +} from 'next/navigation' +import { useTranslation } from 'react-i18next' +import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog' +import CreateAppModal from '@/app/components/app/create-app-modal' +import CreateFromDSLModal, { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal' +import { useProviderContext } from '@/context/provider-context' +import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' + +export type CreateAppCardProps = { + onSuccess?: () => void +} + +// eslint-disable-next-line react/display-name +const CreateAppCard = forwardRef(({ onSuccess }, ref) => { + const { t } = useTranslation() + const { onPlanInfoChanged } = useProviderContext() + const searchParams = useSearchParams() + const { replace } = useRouter() + const dslUrl = searchParams.get('remoteInstallUrl') || undefined + + const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false) + const [showNewAppModal, setShowNewAppModal] = useState(false) + const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(!!dslUrl) + + const activeTab = useMemo(() => { + if (dslUrl) + return CreateFromDSLModalTab.FROM_URL + + return undefined + }, [dslUrl]) + + return ( + +
+
{t('app.createApp')}
+
setShowNewAppModal(true)}> + + {t('app.newApp.startFromBlank')} +
+
setShowNewAppTemplateDialog(true)}> + + {t('app.newApp.startFromTemplate')} +
+
+
setShowCreateFromDSLModal(true)} + > +
+ + {t('app.importDSL')} +
+
+ setShowNewAppModal(false)} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + /> + setShowNewAppTemplateDialog(false)} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + /> + { + setShowCreateFromDSLModal(false) + + if (dslUrl) + replace('/') + }} + activeTab={activeTab} + dslUrl={dslUrl} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + /> +
+ ) +}) + +export default CreateAppCard diff --git a/web/app/(commonLayout)/agent/center/assets/add.svg b/web/app/(commonLayout)/agent/center/assets/add.svg new file mode 100644 index 000000000..9958e855a --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/chat-solid.svg b/web/app/(commonLayout)/agent/center/assets/chat-solid.svg new file mode 100644 index 000000000..a793e982c --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/chat-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/app/(commonLayout)/agent/center/assets/chat.svg b/web/app/(commonLayout)/agent/center/assets/chat.svg new file mode 100644 index 000000000..0971349a5 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/completion-solid.svg b/web/app/(commonLayout)/agent/center/assets/completion-solid.svg new file mode 100644 index 000000000..a9dc7e3dc --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/completion-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/app/(commonLayout)/agent/center/assets/completion.svg b/web/app/(commonLayout)/agent/center/assets/completion.svg new file mode 100644 index 000000000..34af4417f --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/completion.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/discord.svg b/web/app/(commonLayout)/agent/center/assets/discord.svg new file mode 100644 index 000000000..9f22a1ab5 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/github.svg b/web/app/(commonLayout)/agent/center/assets/github.svg new file mode 100644 index 000000000..f03798b5e --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/github.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/web/app/(commonLayout)/agent/center/assets/link-gray.svg b/web/app/(commonLayout)/agent/center/assets/link-gray.svg new file mode 100644 index 000000000..a293cfcf5 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/link-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/link.svg b/web/app/(commonLayout)/agent/center/assets/link.svg new file mode 100644 index 000000000..2926c28b1 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/assets/right-arrow.svg b/web/app/(commonLayout)/agent/center/assets/right-arrow.svg new file mode 100644 index 000000000..a2c1cedf9 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/assets/right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/(commonLayout)/agent/center/hooks/useAppsQueryState.ts b/web/app/(commonLayout)/agent/center/hooks/useAppsQueryState.ts new file mode 100644 index 000000000..fae5357bf --- /dev/null +++ b/web/app/(commonLayout)/agent/center/hooks/useAppsQueryState.ts @@ -0,0 +1,53 @@ +import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useCallback, useEffect, useMemo, useState } from 'react' + +type AppsQuery = { + tagIDs?: string[] + keywords?: string +} + +// Parse the query parameters from the URL search string. +function parseParams(params: ReadonlyURLSearchParams): AppsQuery { + const tagIDs = params.get('tagIDs')?.split(';') + const keywords = params.get('keywords') || undefined + return { tagIDs, keywords } +} + +// Update the URL search string with the given query parameters. +function updateSearchParams(query: AppsQuery, current: URLSearchParams) { + const { tagIDs, keywords } = query || {} + + if (tagIDs && tagIDs.length > 0) + current.set('tagIDs', tagIDs.join(';')) + else + current.delete('tagIDs') + + if (keywords) + current.set('keywords', keywords) + else + current.delete('keywords') +} + +function useAppsQueryState() { + const searchParams = useSearchParams() + const [query, setQuery] = useState(() => parseParams(searchParams)) + + const router = useRouter() + const pathname = usePathname() + const syncSearchParams = useCallback((params: URLSearchParams) => { + const search = params.toString() + const query = search ? `?${search}` : '' + router.push(`${pathname}${query}`) + }, [router, pathname]) + + // Update the URL search string whenever the query changes. + useEffect(() => { + const params = new URLSearchParams(searchParams) + updateSearchParams(query, params) + syncSearchParams(params) + }, [query, searchParams, syncSearchParams]) + + return useMemo(() => ({ query, setQuery }), [query]) +} + +export default useAppsQueryState diff --git a/web/app/(commonLayout)/agent/center/page.tsx b/web/app/(commonLayout)/agent/center/page.tsx new file mode 100644 index 000000000..605a1522f --- /dev/null +++ b/web/app/(commonLayout)/agent/center/page.tsx @@ -0,0 +1,25 @@ +import style from '../../list.module.css' +import Apps from './Apps' +import classNames from '@/utils/classnames' +import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server' + +const AppList = async () => { + const locale = getLocaleOnServer() + const { t } = await translate(locale, 'app') + + return ( +
+ +
+

{t('join')}

+

{t('communityIntro')}

+
+ + +
+
+
+ ) +} + +export default AppList diff --git a/web/app/(commonLayout)/agent/center/style.module.css b/web/app/(commonLayout)/agent/center/style.module.css new file mode 100644 index 000000000..880382ec3 --- /dev/null +++ b/web/app/(commonLayout)/agent/center/style.module.css @@ -0,0 +1,29 @@ + +.commonIcon { + @apply w-4 h-4 inline-block align-middle; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; +} +.actionIcon { + @apply bg-gray-500; + mask-image: url(~@/assets/action.svg); +} +.actionItem { + @apply h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer; + width: calc(100% - 0.5rem); +} +.deleteActionItem { + @apply hover:bg-red-50 !important; +} +.actionName { + @apply text-gray-700 text-sm; +} + +/* .completionPic { + background-image: url(~@/app/components/app-sidebar/completion.png) +} + +.expertPic { + background-image: url(~@/app/components/app-sidebar/expert.png) +} */ diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 2bcc74ec0..56ee61bcf 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -1,4 +1,4 @@ -import { +import React, { memo, useCallback, useState, @@ -25,6 +25,7 @@ import { FileText } from '@/app/components/base/icons/src/vender/line/files' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' import type { InputVar } from '@/app/components/workflow/types' import { appDefaultIconBackground } from '@/config' +import { SimpleSelect } from '@/app/components/base/select' export type AppPublisherProps = { disabled?: boolean @@ -41,13 +42,18 @@ export type AppPublisherProps = { crossAxisOffset?: number toolPublished?: boolean inputs?: InputVar[] + selects: any[] onRefreshData?: () => void + onSelect: (item: string) => void + onAgentAddAndDelete: (status: number) => void + detail: any } const AppPublisher = ({ disabled = false, publishDisabled = false, publishedAt, + selects, draftUpdatedAt, debugWithMultipleModel = false, multipleModelConfigs = [], @@ -58,6 +64,9 @@ const AppPublisher = ({ toolPublished, inputs, onRefreshData, + onAgentAddAndDelete, + onSelect, + detail, }: AppPublisherProps) => { const { t } = useTranslation() const [published, setPublished] = useState(false) @@ -94,7 +103,6 @@ const AppPublisher = ({ const handleTrigger = useCallback(() => { const state = !open - if (disabled) { setOpen(false) return @@ -106,9 +114,7 @@ const AppPublisher = ({ if (state) setPublished(false) }, [disabled, onToggle, open]) - const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false) - return ( -
+
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedTime)}
- }>{t('workflow.common.runApp')} +
+ 智能体中心 +
+
+ { + if (!detail?.agentTypeId) + onSelect(item.value) + } + } + items={selects.map((item) => { + return { + value: item.id, + name: item.name, + } + })} + /> +
+
+ + +
+
+
+ }>{t('workflow.common.runApp')} {appDetail?.mode === 'workflow' ? ( } + icon={} > {t('workflow.common.batchRunApp')} @@ -202,12 +256,13 @@ const AppPublisher = ({ handleTrigger() }} disabled={!publishedTime} - icon={} + icon={} > {t('workflow.common.embedIntoSite')} )} - }>{t('workflow.common.accessAPIReference')} + }>{t('workflow.common.accessAPIReference')} {appDetail?.mode === 'workflow' && ( - + ) } diff --git a/web/app/components/header/agent-nav/index.tsx b/web/app/components/header/agent-nav/index.tsx new file mode 100644 index 000000000..55754035a --- /dev/null +++ b/web/app/components/header/agent-nav/index.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useTranslation } from 'react-i18next' +import Link from 'next/link' +import { useSelectedLayoutSegment } from 'next/navigation' +import { + RiRobot2Fill, RiRobot2Line, +} from '@remixicon/react' +import classNames from '@/utils/classnames' +type AgentNavProps = { + className?: string +} + +const AgentNav = ({ + className, +}: AgentNavProps) => { + const { t } = useTranslation() + const selectedSegment = useSelectedLayoutSegment() + const actived = selectedSegment === 'agent' + + return ( + + { + actived + ? + : + } + {t('common.menus.agent')} + + ) +} + +export default AgentNav diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 2b020b81e..d297cb303 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -18,6 +18,7 @@ import LogoSite from '@/app/components/base/logo/logo-site' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' +import AgentNav from '@/app/components/header/agent-nav' const navClassName = ` flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl @@ -76,6 +77,7 @@ const Header = () => { {!isCurrentWorkspaceDatasetOperator && } {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } {!isCurrentWorkspaceDatasetOperator && } + {!isCurrentWorkspaceDatasetOperator && }
)}
@@ -95,6 +97,7 @@ const Header = () => { {!isCurrentWorkspaceDatasetOperator && } {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && } {!isCurrentWorkspaceDatasetOperator && } + {!isCurrentWorkspaceDatasetOperator && }
)}
diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index 58624d816..2fa011ec6 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -3,6 +3,7 @@ import { memo, useCallback, useMemo, + useState, } from 'react' import { RiApps2AddLine } from '@remixicon/react' import { useNodes } from 'reactflow' @@ -39,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { publishWorkflow } from '@/service/workflow' import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' import { useFeatures } from '@/app/components/base/features/hooks' +import { agentAdd, getAgentTypeList, getDifyList } from '@/service/agent' const Header: FC = () => { const { t } = useTranslation() @@ -55,6 +57,9 @@ const Header: FC = () => { const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startVariables = startNode?.data.variables const fileSettings = useFeatures(s => s.features.file) + const [selects, setSelects] = useState([]) + const [agentType, setAgentType] = useState() + const [detail, setDetail] = useState() const variables = useMemo(() => { const data = startVariables || [] if (fileSettings?.image?.enabled) { @@ -96,7 +101,6 @@ const Header: FC = () => { return setShowFeaturesPanel(!showFeaturesPanel) }, [workflowStore, getNodesReadOnly]) - const handleCancelRestore = useCallback(() => { handleLoadBackupDraft() workflowStore.setState({ isRestoring: false }) @@ -121,27 +125,55 @@ const Header: FC = () => { throw new Error('Checklist failed') } }, [appID, handleCheckBeforePublish, notify, t, workflowStore]) + const getData = useCallback(async () => { + const res: any = await getAgentTypeList('/dify/agent-type/list') + setSelects(res.data) + const detail: any = await getDifyList('/dify/list', { + appId: appID, + }, + ) + setDetail(detail.data[0]) + setAgentType(detail.data[0].agentTypeId) + }, [appID]) + const onAgentAddAndDelete = useCallback(async (status: number) => { + if (!agentType && status === 1) { + notify({ type: 'error', message: '请选择类型' }) + return + } + const url = '/dify/agent/up-down-shelves' + const res = await agentAdd(url, { + agentTypeId: agentType, + appId: appID, + sort: 0, + status, + }) + if (res.code === 'Success') { + await getData() + notify({ type: 'success', message: t('common.api.actionSuccess') }) + } + else { notify({ type: 'error', message: res.message }) } + }, [agentType, appID, detail, getData, notify, t]) const onStartRestoring = useCallback(() => { workflowStore.setState({ isRestoring: true }) handleBackupDraft() handleRestoreFromPublishedWorkflow() }, [handleBackupDraft, handleRestoreFromPublishedWorkflow, workflowStore]) - - const onPublisherToggle = useCallback((state: boolean) => { - if (state) + const onPublisherToggle = useCallback(async (state: boolean) => { + if (state) { handleSyncWorkflowDraft(true) - }, [handleSyncWorkflowDraft]) - + console.log(123, 12) + await getData() + } + }, [getData, handleSyncWorkflowDraft]) const handleGoBackToEdit = useCallback(() => { handleLoadBackupDraft() workflowStore.setState({ historyWorkflowData: undefined }) }, [workflowStore, handleLoadBackupDraft]) - const handleToolConfigureUpdate = useCallback(() => { + const handleToolConfigureUpdate = useCallback(async () => { workflowStore.setState({ toolPublished: true }) }, [workflowStore]) - return (
{ inputs: variables, onRefreshData: handleToolConfigureUpdate, onPublish, + onAgentAddAndDelete, + selects, + detail, + onSelect: (id: string) => { + setAgentType(id) + }, onRestore: onStartRestoring, onToggle: onPublisherToggle, crossAxisOffset: 4, diff --git a/web/config/index.ts b/web/config/index.ts index 12e66a08d..97031b6ac 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -23,7 +23,7 @@ else { // const domainParts = globalThis.location?.host?.split('.'); // in production env, the host is dify.app . In other env, the host is [dev].dify.app // const env = domainParts.length === 2 ? 'ai' : domainParts?.[0]; - apiPrefix = 'http://localhost:5001/console/api' + apiPrefix = 'https://aistock-retail.test.investoday.net/agents/console/api' publicApiPrefix = 'http://localhost:5001/api' // avoid browser private mode api cross origin } diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 477dd363c..f6c53749a 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -125,6 +125,7 @@ const translation = { newApp: '创建应用', newDataset: '创建知识库', tools: '工具', + agent: '智能体中心', }, userProfile: { settings: '设置', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 56d1de6ce..a2fc50664 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -7,6 +7,7 @@ const translation = { unpublished: '未发布', published: '已发布', publish: '发布', + delist: '下架', update: '更新', run: '运行', running: '运行中', diff --git a/web/service/agent.ts b/web/service/agent.ts new file mode 100644 index 000000000..1ae790cc2 --- /dev/null +++ b/web/service/agent.ts @@ -0,0 +1,73 @@ +import type { Fetcher } from 'swr' +import { get, post } from './base' + +/** + * agent类型列表 + * @param url + * @param param + */ +export const getAgentTypeList: Fetcher = (url: string) => { + return get(url, {}, { + isPublicAPI: true, + }) +} + +/** + * 发布、下架 + * @param url + * @param data + */ +export const agentAdd: (url: string, data: any) => Promise = (url: string, data: any) => { + return post(url, { + body: data, + }, { + isPublicAPI: true, + }) +} + +/** + * 智能体列表 + * @param url + * @param params + */ +export const getDifyList = (url: string, params: any) => { + return get(url, { + params, + }, { + isPublicAPI: true, + }) +} + +/** + * 添加类型 + * @param url + * @param data + */ +export const agentTypeAdd: (url: string, data: any) => Promise = (url: string, data: any) => { + return post(url, { + body: data, + }, { + isPublicAPI: true, + }) +} + +/** + * 删除类型 + * @param url + * @param params + */ +export const agentTypeDelete = (url: string, params: any) => { + return get(url, { + params, + }, { + isPublicAPI: true, + }) +} + +export const agentUpdateSortBatch: (url: string, data: any) => Promise = (url: string, data: any) => { + return post(url, { + body: data, + }, { + isPublicAPI: true, + }) +} diff --git a/web/service/base.ts b/web/service/base.ts index 031096f2c..5cc693582 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -280,16 +280,18 @@ const baseFetch = ( options.signal = abortController.signal } if (isPublicAPI) { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) - let accessTokenJson = { [sharedToken]: '' } - try { - accessTokenJson = JSON.parse(accessToken) - } - catch (e) { - - } - options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`) + // const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] + // const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) + // let accessTokenJson = { [sharedToken]: '' } + // try { + // accessTokenJson = JSON.parse(accessToken) + // } + // catch (e) { + // + // } + // options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`) + const accessToken = localStorage.getItem('console_token') || '' + options.headers.set('Authorization', `Bearer ${accessToken}`) } else { const accessToken = localStorage.getItem('console_token') || '' @@ -323,7 +325,6 @@ const baseFetch = ( delete options.params } - if (body && bodyStringify) options.body = JSON.stringify(body) From 11ea737cbf46e18ea9745ce4d14e44ae59288f7d Mon Sep 17 00:00:00 2001 From: ade <2329571595@qq.com> Date: Wed, 18 Sep 2024 23:51:08 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E4=B8=AD=E5=BF=83=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/.env | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 web/.env diff --git a/web/.env b/web/.env new file mode 100644 index 000000000..614ab798f --- /dev/null +++ b/web/.env @@ -0,0 +1,21 @@ +# For production release, change this to PRODUCTION +NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT +# The deployment edition, SELF_HOSTED +NEXT_PUBLIC_EDITION=SELF_HOSTED +# The base URL of console application, refers to the Console base URL of WEB service if console domain is +# different from api or web app domain. +# example: http://cloud.dify.ai/console/api +NEXT_PUBLIC_API_PREFIX=https://aistock-retail.test.investoday.net/agents/console/api +# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from +# console or api domain. +# example: http://udify.app/api +NEXT_PUBLIC_PUBLIC_API_PREFIX=https://aistock-retail.test.investoday.net/llm-base2 + +# SENTRY +NEXT_PUBLIC_SENTRY_DSN= + +# Disable Next.js Telemetry (https://nextjs.org/telemetry) +NEXT_TELEMETRY_DISABLED=1 + +# Disable Upload Image as WebApp icon default is false +NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false From 03425b27c1a64d1c3ad0829a80d925767d7b878c Mon Sep 17 00:00:00 2001 From: kenneth Date: Thu, 19 Sep 2024 13:27:55 +0800 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20.env=E6=96=87=E4=BB=B6=E4=B8=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E6=8F=90=E4=BA=A4=EF=BC=8C=E8=AF=B7=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20.env.local=20add:=20.gitignore=E7=9A=84=E6=8E=92?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/.env | 21 --------------------- web/.gitignore | 3 ++- 2 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 web/.env diff --git a/web/.env b/web/.env deleted file mode 100644 index 614ab798f..000000000 --- a/web/.env +++ /dev/null @@ -1,21 +0,0 @@ -# For production release, change this to PRODUCTION -NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT -# The deployment edition, SELF_HOSTED -NEXT_PUBLIC_EDITION=SELF_HOSTED -# The base URL of console application, refers to the Console base URL of WEB service if console domain is -# different from api or web app domain. -# example: http://cloud.dify.ai/console/api -NEXT_PUBLIC_API_PREFIX=https://aistock-retail.test.investoday.net/agents/console/api -# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from -# console or api domain. -# example: http://udify.app/api -NEXT_PUBLIC_PUBLIC_API_PREFIX=https://aistock-retail.test.investoday.net/llm-base2 - -# SENTRY -NEXT_PUBLIC_SENTRY_DSN= - -# Disable Next.js Telemetry (https://nextjs.org/telemetry) -NEXT_TELEMETRY_DISABLED=1 - -# Disable Upload Image as WebApp icon default is false -NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false diff --git a/web/.gitignore b/web/.gitignore index 9dd00b1b0..4ffb961a6 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -29,6 +29,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel @@ -49,4 +50,4 @@ package-lock.json # pmpm pnpm-lock.yaml -.favorites.json \ No newline at end of file +.favorites.json From 6f657b9d6457d49aff919a3ef9f152679c9dee3b Mon Sep 17 00:00:00 2001 From: kenneth Date: Thu, 19 Sep 2024 13:50:40 +0800 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20=E6=8E=A5=E5=8F=A3=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=80=9A=E8=BF=87customAPI=E6=9D=A5=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/service/agent.ts | 6 ++++-- web/service/base.ts | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/service/agent.ts b/web/service/agent.ts index 1ae790cc2..4e312bdab 100644 --- a/web/service/agent.ts +++ b/web/service/agent.ts @@ -1,14 +1,16 @@ import type { Fetcher } from 'swr' import { get, post } from './base' +const baseURL = '/llm-base' + /** * agent类型列表 * @param url * @param param */ export const getAgentTypeList: Fetcher = (url: string) => { - return get(url, {}, { - isPublicAPI: true, + return get(`${baseURL}/dify/agent-type/list`, {}, { + customAPI: true, }) } diff --git a/web/service/base.ts b/web/service/base.ts index 3b90d2513..b58707d44 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -70,6 +70,7 @@ export type IOnTextReplace = (textReplace: TextReplaceResponse) => void export type IOtherOptions = { isPublicAPI?: boolean + customAPI?: boolean // 自定义API bodyStringify?: boolean needAllResponseContent?: boolean deleteContentType?: boolean @@ -280,6 +281,7 @@ const baseFetch = ( fetchOptions: FetchOptionType, { isPublicAPI = false, + customAPI = false, bodyStringify = true, needAllResponseContent, deleteContentType, @@ -321,7 +323,7 @@ const baseFetch = ( options.headers.set('Content-Type', ContentType.json) } - const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX + const urlPrefix = customAPI ? '' : isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}` const { method, params, body } = options From bd6fb808384cf6adbaccd6be837baae7e33643c3 Mon Sep 17 00:00:00 2001 From: ade <2329571595@qq.com> Date: Thu, 19 Sep 2024 19:57:43 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E5=9B=BA=E5=AE=9Aurl=E3=80=81=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=B1=BB=E5=9E=8B=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/center/AddAgentType.tsx | 2 +- .../(commonLayout)/agent/center/AppCard.tsx | 422 ------------------ web/app/(commonLayout)/agent/center/Apps.tsx | 52 ++- .../agent/center/DraggableList.tsx | 4 +- .../agent/center/DraggableTypeList.tsx | 106 +++++ .../agent/center/EditAgentType.tsx | 4 +- web/app/components/workflow/header/index.tsx | 6 +- web/app/layout.tsx | 1 + web/config/index.ts | 10 +- web/service/agent.ts | 73 +-- web/service/base.ts | 30 +- 11 files changed, 212 insertions(+), 498 deletions(-) delete mode 100644 web/app/(commonLayout)/agent/center/AppCard.tsx create mode 100644 web/app/(commonLayout)/agent/center/DraggableTypeList.tsx diff --git a/web/app/(commonLayout)/agent/center/AddAgentType.tsx b/web/app/(commonLayout)/agent/center/AddAgentType.tsx index 57285431e..47550d8b7 100644 --- a/web/app/(commonLayout)/agent/center/AddAgentType.tsx +++ b/web/app/(commonLayout)/agent/center/AddAgentType.tsx @@ -28,7 +28,7 @@ const AddAgentType = ({ Toast.notify({ type: 'error', message: '请输入类型名称' }) return } - agentTypeAdd('/dify/agent-type/add', { + agentTypeAdd({ name, description, sort, diff --git a/web/app/(commonLayout)/agent/center/AppCard.tsx b/web/app/(commonLayout)/agent/center/AppCard.tsx deleted file mode 100644 index fb39dee5a..000000000 --- a/web/app/(commonLayout)/agent/center/AppCard.tsx +++ /dev/null @@ -1,422 +0,0 @@ -'use client' - -import { useContext, useContextSelector } from 'use-context-selector' -import { useRouter } from 'next/navigation' -import { useCallback, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { RiMoreFill } from '@remixicon/react' -import s from './style.module.css' -import cn from '@/utils/classnames' -import type { App } from '@/types/app' -import Confirm from '@/app/components/base/confirm' -import { ToastContext } from '@/app/components/base/toast' -import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' -import DuplicateAppModal from '@/app/components/app/duplicate-modal' -import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' -import AppIcon from '@/app/components/base/app-icon' -import AppsContext, { useAppContext } from '@/context/app-context' -import type { HtmlContentProps } from '@/app/components/base/popover' -import CustomPopover from '@/app/components/base/popover' -import Divider from '@/app/components/base/divider' -import { getRedirection } from '@/utils/app-redirection' -import { useProviderContext } from '@/context/provider-context' -import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication' -import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' -import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' -import EditAppModal from '@/app/components/explore/create-app-modal' -import SwitchAppModal from '@/app/components/app/switch-app-modal' -import type { Tag } from '@/app/components/base/tag-management/constant' -import TagSelector from '@/app/components/base/tag-management/selector' -import type { EnvironmentVariable } from '@/app/components/workflow/types' -import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' -import { fetchWorkflowDraft } from '@/service/workflow' - -export type AppCardProps = { - app: App - onRefresh?: () => void -} - -const AppCard = ({ app, onRefresh }: AppCardProps) => { - const { t } = useTranslation() - const { notify } = useContext(ToastContext) - const { isCurrentWorkspaceEditor } = useAppContext() - const { onPlanInfoChanged } = useProviderContext() - const { push } = useRouter() - - const mutateApps = useContextSelector( - AppsContext, - state => state.mutateApps, - ) - - const [showEditModal, setShowEditModal] = useState(false) - const [showDuplicateModal, setShowDuplicateModal] = useState(false) - const [showSwitchModal, setShowSwitchModal] = useState(false) - const [showConfirmDelete, setShowConfirmDelete] = useState(false) - const [secretEnvList, setSecretEnvList] = useState([]) - - const onConfirmDelete = useCallback(async () => { - try { - await deleteApp(app.id) - notify({ type: 'success', message: t('app.appDeleted') }) - if (onRefresh) - onRefresh() - mutateApps() - onPlanInfoChanged() - } - catch (e: any) { - notify({ - type: 'error', - message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`, - }) - } - setShowConfirmDelete(false) - }, [app.id]) - - const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ - name, - icon_type, - icon, - icon_background, - description, - use_icon_as_answer_icon, - }) => { - try { - await updateAppInfo({ - appID: app.id, - name, - icon_type, - icon, - icon_background, - description, - use_icon_as_answer_icon, - }) - setShowEditModal(false) - notify({ - type: 'success', - message: t('app.editDone'), - }) - if (onRefresh) - onRefresh() - mutateApps() - } - catch (e) { - notify({ type: 'error', message: t('app.editFailed') }) - } - }, [app.id, mutateApps, notify, onRefresh, t]) - - const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { - try { - const newApp = await copyApp({ - appID: app.id, - name, - icon_type, - icon, - icon_background, - mode: app.mode, - }) - setShowDuplicateModal(false) - notify({ - type: 'success', - message: t('app.newApp.appCreated'), - }) - localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') - if (onRefresh) - onRefresh() - mutateApps() - onPlanInfoChanged() - getRedirection(isCurrentWorkspaceEditor, newApp, push) - } - catch (e) { - notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) - } - } - - const onExport = async (include = false) => { - try { - const { data } = await exportAppConfig({ - appID: app.id, - include, - }) - const a = document.createElement('a') - const file = new Blob([data], { type: 'application/yaml' }) - a.href = URL.createObjectURL(file) - a.download = `${app.name}.yml` - a.click() - } - catch (e) { - notify({ type: 'error', message: t('app.exportFailed') }) - } - } - - const exportCheck = async () => { - if (app.mode !== 'workflow' && app.mode !== 'advanced-chat') { - onExport() - return - } - try { - const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`) - const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') - if (list.length === 0) { - onExport() - return - } - setSecretEnvList(list) - } - catch (e) { - notify({ type: 'error', message: t('app.exportFailed') }) - } - } - - const onSwitch = () => { - if (onRefresh) - onRefresh() - mutateApps() - setShowSwitchModal(false) - } - - const Operations = (props: HtmlContentProps) => { - const onMouseLeave = async () => { - props.onClose?.() - } - const onClickSettings = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowEditModal(true) - } - const onClickDuplicate = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowDuplicateModal(true) - } - const onClickExport = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - exportCheck() - } - const onClickSwitch = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowSwitchModal(true) - } - const onClickDelete = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowConfirmDelete(true) - } - return ( -
- - - - - {(app.mode === 'completion' || app.mode === 'chat') && ( - <> - -
- {t('app.switch')} -
- - )} - -
- - {t('common.operation.delete')} - -
-
- ) - } - - const [tags, setTags] = useState(app.tags) - useEffect(() => { - setTags(app.tags) - }, [app.tags]) - - return ( - <> -
{ - e.preventDefault() - getRedirection(isCurrentWorkspaceEditor, app, push) - }} - className='relative group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' - > -
-
- - - {app.mode === 'advanced-chat' && ( - - )} - {app.mode === 'agent-chat' && ( - - )} - {app.mode === 'chat' && ( - - )} - {app.mode === 'completion' && ( - - )} - {app.mode === 'workflow' && ( - - )} - -
-
-
-
{app.name}
-
-
- {app.mode === 'advanced-chat' &&
{t('app.types.chatbot').toUpperCase()}
} - {app.mode === 'chat' &&
{t('app.types.chatbot').toUpperCase()}
} - {app.mode === 'agent-chat' &&
{t('app.types.agent').toUpperCase()}
} - {app.mode === 'workflow' &&
{t('app.types.workflow').toUpperCase()}
} - {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
} -
-
-
-
-
- {app.description} -
-
-
- {isCurrentWorkspaceEditor && ( - <> -
{ - e.stopPropagation() - e.preventDefault() - }}> -
- tag.id)} - selectedTags={tags} - onCacheUpdate={setTags} - onChange={onRefresh} - /> -
-
-
-
- } - position="br" - trigger="click" - btnElement={ -
- -
- } - btnClassName={open => - cn( - open ? '!bg-black/5 !shadow-none' : '!bg-transparent', - 'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5', - ) - } - popupClassName={ - (app.mode === 'completion' || app.mode === 'chat') - ? '!w-[238px] translate-x-[-110px]' - : '' - } - className={'!w-[128px] h-fit !z-20'} - /> -
- - )} -
-
- {showEditModal && ( - setShowEditModal(false)} - /> - )} - {showDuplicateModal && ( - setShowDuplicateModal(false)} - /> - )} - {showSwitchModal && ( - setShowSwitchModal(false)} - onSuccess={onSwitch} - /> - )} - {showConfirmDelete && ( - setShowConfirmDelete(false)} - /> - )} - {secretEnvList.length > 0 && ( - setSecretEnvList([])} - /> - )} - - ) -} - -export default AppCard diff --git a/web/app/(commonLayout)/agent/center/Apps.tsx b/web/app/(commonLayout)/agent/center/Apps.tsx index f66d57ec3..f553e063a 100644 --- a/web/app/(commonLayout)/agent/center/Apps.tsx +++ b/web/app/(commonLayout)/agent/center/Apps.tsx @@ -9,7 +9,6 @@ import { RiRobot2Line, RiRobot3Line } from '@remixicon/react' import cn from 'classnames' import { Pagination } from 'react-headless-pagination' import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' -import Image from 'next/image' import useAppsQueryState from './hooks/useAppsQueryState' import { APP_PAGE_LIMIT, NEED_REFRESH_APP_LIST_KEY } from '@/config' import { CheckModal } from '@/hooks/use-pay' @@ -24,6 +23,7 @@ import Toast from '@/app/components/base/toast' import EditAgentType from '@/app/(commonLayout)/agent/center/EditAgentType' import DragDropSort from '@/app/(commonLayout)/agent/center/DraggableList' import Confirm from '@/app/components/base/confirm' +import DragDropSortType from '@/app/(commonLayout)/agent/center/DraggableTypeList' const getKey = ( activeTab: string, @@ -58,10 +58,21 @@ const Apps = () => { const [showAddAgentModal, setShowAddAgentModal] = useState(false) const [showEditAgentModal, setShowEditAgentModal] = useState(false) const [showDrag, setShowDrag] = useState(false) + const [showDragType, setShowDragType] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [deleteId, setDeleteId] = useState('') + const [difys, setDifys] = useState([]) + const [difysTotal, setDifysTotal] = useState(0) + const [row, setRow] = useState() + const getDifys = () => { + getDifyList({ name: searchKeywords, agentTypeId: activeTab, page: difysCurrPage + 1, pageSize: APP_PAGE_LIMIT }).then((res) => { + setDifys(res.data) + console.log(res.data) + setDifysTotal(res.totalCount) + }) + } const getTypes = () => { - getAgentTypeList('/dify/agent-type/list').then((res: any) => { + getAgentTypeList({ page: currPage + 1, pageSize: 999999 }).then((res: any) => { setOptions(res.data.map((item: any) => { return { value: item.id, @@ -73,20 +84,11 @@ const Apps = () => { }) } const getOptTypes = () => { - getDifyList('/dify/agent-type/list', { page: currPage + 1, pageSize: APP_PAGE_LIMIT }).then((res: any) => { + getAgentTypeList({ page: currPage + 1, pageSize: APP_PAGE_LIMIT }).then((res: any) => { setTotal(res.totalCount) setTypes(res.data) }) } - const [difys, setDifys] = useState([]) - const [difysTotal, setDifysTotal] = useState(0) - const [row, setRow] = useState() - const getDifys = () => { - getDifyList('/dify/list', { name: searchKeywords, agentTypeId: activeTab, page: difysCurrPage + 1, pageSize: APP_PAGE_LIMIT }).then((res) => { - setDifys(res.data) - setDifysTotal(res.totalCount) - }) - } useEffect(() => { document.title = '智能体中心 - Dify' @@ -96,8 +98,7 @@ const Apps = () => { } getTypes() getOptTypes() - getDifys() - }, [mutate, t]) + }, [mutate]) const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) @@ -113,7 +114,7 @@ const Apps = () => { useEffect(() => { getDifys() - }, [difysCurrPage, searchKeywords]) + }, [difysCurrPage, searchKeywords, activeTab]) return ( <>
@@ -146,10 +147,10 @@ const Apps = () => {