From 17a3db3362d7e2ae403d69983e233ffff50e83c5 Mon Sep 17 00:00:00 2001 From: kun Date: Wed, 8 Jan 2025 22:25:50 +0800 Subject: [PATCH 1/8] feat: delete manager --- web/apps/web/next.config.js | 6 +- web/apps/web/src/app/container.ts | 18 - .../app/plugins/comfyui/comfyui.model.ts | 55 +-- .../app/plugins/comfyui/services/index.tsx | 14 + .../app/plugins/comfyui/services/type.tsx | 1 + .../components/chat/app-builder-chat.model.ts | 18 +- .../common/list-footer-extra/index.tsx | 6 +- .../components/download/completed/index.tsx | 62 --- .../download/download-definitions.ts | 28 -- .../src/components/download/download.model.ts | 146 ------- .../components/download/downloading/index.tsx | 92 ---- .../web/src/components/download/index.tsx | 75 ---- web/apps/web/src/components/manager/index.tsx | 34 -- .../manager/manager-content/index.tsx | 62 --- .../manager/manager-content/manager-confg.ts | 1 - .../models/models-common.model.ts | 164 ------- .../models/models-installed.model.ts | 97 ----- .../models/models-installed.tsx | 209 --------- .../models/models-marketplace.model.ts | 85 ---- .../models/models-marketplace.tsx | 405 ------------------ .../widgets/widgets-common.model.ts | 246 ----------- .../widgets/widgets-installed.model.ts | 154 ------- .../widgets/widgets-installed.tsx | 128 ------ .../widgets/widgets-marketplace.model.ts | 42 -- .../widgets/widgets-marketplace.tsx | 396 ----------------- .../components/manager/manager-definitions.ts | 70 --- .../components/manager/manger-sider/index.tsx | 42 -- .../src/components/settings/settings.model.ts | 123 ++---- web/apps/web/src/services/app/index.tsx | 94 ++++ web/apps/web/src/services/app/message-type.ts | 1 + .../web/src/stores/global/global-provider.tsx | 2 - 31 files changed, 181 insertions(+), 2695 deletions(-) delete mode 100644 web/apps/web/src/components/download/completed/index.tsx delete mode 100644 web/apps/web/src/components/download/download-definitions.ts delete mode 100644 web/apps/web/src/components/download/download.model.ts delete mode 100644 web/apps/web/src/components/download/downloading/index.tsx delete mode 100644 web/apps/web/src/components/download/index.tsx delete mode 100644 web/apps/web/src/components/manager/index.tsx delete mode 100644 web/apps/web/src/components/manager/manager-content/index.tsx delete mode 100644 web/apps/web/src/components/manager/manager-content/manager-confg.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/models/models-common.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/models/models-installed.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/models/models-installed.tsx delete mode 100644 web/apps/web/src/components/manager/manager-content/models/models-marketplace.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/models/models-marketplace.tsx delete mode 100644 web/apps/web/src/components/manager/manager-content/widgets/widgets-common.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.tsx delete mode 100644 web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.model.ts delete mode 100644 web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.tsx delete mode 100644 web/apps/web/src/components/manager/manager-definitions.ts delete mode 100644 web/apps/web/src/components/manager/manger-sider/index.tsx diff --git a/web/apps/web/next.config.js b/web/apps/web/next.config.js index 2aca1ce3..34dfd9d6 100644 --- a/web/apps/web/next.config.js +++ b/web/apps/web/next.config.js @@ -25,7 +25,6 @@ module.exports = { ); const destFile = path.join(__dirname, 'public/fabric.js'); copyFileSync(srcFile, destFile); - console.log('copied fabric.js'); // Important: return the modified config config.resolve.alias = { @@ -54,7 +53,10 @@ module.exports = { return [ { source: '/api/:path*', - destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`, + destination: `${ + process.env.NEXT_PUBLIC_SHELL_API_URL || + process.env.NEXT_PUBLIC_API_URL + }/api/:path*`, }, { source: '/models_searcher/:path*', diff --git a/web/apps/web/src/app/container.ts b/web/apps/web/src/app/container.ts index c6e6933e..f2b97eef 100644 --- a/web/apps/web/src/app/container.ts +++ b/web/apps/web/src/app/container.ts @@ -11,14 +11,7 @@ import { ComfyUIModel } from '@/components/app/plugins/comfyui/comfyui.model'; import { AssistantModel } from '@/components/assistant/model'; import { FeedbackModel } from '@/components/feedback/model'; import { AppBuilderChatModel } from '@/components/chat/app-builder-chat.model'; -import { DownloadModel } from '@/components/download/download.model'; import { OpenImageCanvasModel } from '@/components/image-canvas/open-image-canvas.model'; -import { ModelsCommonModel } from '@/components/manager/manager-content/models/models-common.model'; -import { ModelsInstalledModel } from '@/components/manager/manager-content/models/models-installed.model'; -import { ModelsMarketplaceModel } from '@/components/manager/manager-content/models/models-marketplace.model'; -import { WidgetsCommonModel } from '@/components/manager/manager-content/widgets/widgets-common.model'; -import { WidgetsInstalledModel } from '@/components/manager/manager-content/widgets/widgets-installed.model'; -import { WidgetsMarketplaceModel } from '@/components/manager/manager-content/widgets/widgets-marketplace.model'; import { SettingsModel } from '@/components/settings/settings.model'; import { AppBuilderModel } from '@/stores/app/models/app-builder.model'; import { FormEngineModel } from '@/utils/form-engine.model'; @@ -79,23 +72,12 @@ container return model; }); -// manager -container.bind(WidgetsCommonModel).toSelf().inSingletonScope(); -container.bind(WidgetsMarketplaceModel).toSelf().inSingletonScope(); -container.bind(WidgetsInstalledModel).toSelf().inSingletonScope(); -container.bind(ModelsCommonModel).toSelf().inSingletonScope(); -container.bind(ModelsMarketplaceModel).toSelf().inSingletonScope(); -container.bind(ModelsInstalledModel).toSelf().inSingletonScope(); - // settings container.bind(SettingsModel).toSelf().inSingletonScope(); // app builder chat container.bind(AppBuilderChatModel).toSelf().inSingletonScope(); -// download -container.bind(DownloadModel).toSelf().inSingletonScope(); - // image canvas container.bind('ImageCanvasModel').to(ImageCanvasModel).inSingletonScope(); container.bind(OpenImageCanvasModel).toSelf().inSingletonScope(); diff --git a/web/apps/web/src/components/app/plugins/comfyui/comfyui.model.ts b/web/apps/web/src/components/app/plugins/comfyui/comfyui.model.ts index 604fd3da..9b55b942 100644 --- a/web/apps/web/src/components/app/plugins/comfyui/comfyui.model.ts +++ b/web/apps/web/src/components/app/plugins/comfyui/comfyui.model.ts @@ -1,5 +1,4 @@ import { customSnakeCase } from '@shellagent/shared/utils'; -import axios from 'axios'; import { inject, injectable } from 'inversify'; import { isEmpty, merge, omit } from 'lodash-es'; import mitt from 'mitt'; @@ -12,7 +11,13 @@ import { LOCATION_TIP, MessageType, } from '@/components/app/plugins/comfyui/constant'; -import { getFile, saveComfy } from '@/components/app/plugins/comfyui/services'; +import { + checkJsonExist, + getFile, + saveComfy, + getCwdSvc, + updateDependency, +} from '@/components/app/plugins/comfyui/services'; import { GetFileResponse, type SaveRequest, @@ -194,18 +199,10 @@ export class ComfyUIModel { @action.bound async checkJsonExists(location: string) { try { - const res = await axios.post( - `/api/comfyui/check_json_exist`, - { - location, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (!res.data.success) { + const result = await checkJsonExist({ + location, + }); + if (!result.success) { return `The ShellAgent-extended ComfyUI JSON (.shellagent.json) doesn't exist`; } return undefined; @@ -217,13 +214,9 @@ export class ComfyUIModel { @action.bound async getCwd() { try { - const res = await axios.get(`/api/get_cwd`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - if (typeof res.data.cwd === 'string') { - this.defaultLocation = pathJoin([res.data.cwd, 'data/comfy_workflow']); + const result = await getCwdSvc({}); + if (typeof result.cwd === 'string') { + this.defaultLocation = pathJoin([result.cwd, 'data/comfy_workflow']); } else { this.toast.error(`/api/get_cwd response is invalid`); } @@ -294,20 +287,12 @@ export class ComfyUIModel { @action.bound async duplicateComfyUIExtendedJson(location: string, location_new: string) { try { - await axios.post( - `/api/comfyui/update_dependency`, - { - location, - location_new, // this param will duplicate a json from location - missing_custom_nodes: [], - missing_models: {}, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + await updateDependency({ + location, + location_new, // this param will duplicate a json from location + missing_custom_nodes: [], + missing_models: {}, + }); } catch (e) { this.toast.error((e as Error).message); throw e; diff --git a/web/apps/web/src/components/app/plugins/comfyui/services/index.tsx b/web/apps/web/src/components/app/plugins/comfyui/services/index.tsx index 94c64c9d..787b6576 100644 --- a/web/apps/web/src/components/app/plugins/comfyui/services/index.tsx +++ b/web/apps/web/src/components/app/plugins/comfyui/services/index.tsx @@ -1,4 +1,5 @@ import { APIFetch } from '@/services/base'; +import type { Fetcher } from 'swr'; import { GetFileRequest, @@ -37,3 +38,16 @@ export const updateDependency = (params: UpdateDependencyRequest) => { }, ); }; + +export const checkJsonExist = (params: { location: string }) => { + return APIFetch.post( + '/api/comfyui/check_json_exist', + { + body: params, + }, + ); +}; + +export const getCwdSvc: Fetcher<{ cwd: string }, {}> = () => { + return APIFetch.get('/api/get_cwd'); +}; diff --git a/web/apps/web/src/components/app/plugins/comfyui/services/type.tsx b/web/apps/web/src/components/app/plugins/comfyui/services/type.tsx index 0420e148..e5c6e1bb 100644 --- a/web/apps/web/src/components/app/plugins/comfyui/services/type.tsx +++ b/web/apps/web/src/components/app/plugins/comfyui/services/type.tsx @@ -141,6 +141,7 @@ export interface GetFileResponse { export interface UpdateDependencyRequest { comfy_workflow_id?: string; location?: string; + location_new?: string; missing_custom_nodes: Array<{ name: string; repo: string; diff --git a/web/apps/web/src/components/chat/app-builder-chat.model.ts b/web/apps/web/src/components/chat/app-builder-chat.model.ts index 428594d6..8c5b9c20 100644 --- a/web/apps/web/src/components/chat/app-builder-chat.model.ts +++ b/web/apps/web/src/components/chat/app-builder-chat.model.ts @@ -4,7 +4,6 @@ import { } from '@microsoft/fetch-event-source'; import { Automata } from '@shellagent/pro-config'; import { ChatNewModel } from '@shellagent/ui'; -import axios from 'axios'; import { inject, injectable } from 'inversify'; import { action, makeObservable, observable, runInAction } from 'mobx'; import { ButtonFnParams, IMLocalFile } from 'myshell-bundled-chat'; @@ -21,6 +20,7 @@ import { import type { ServerMessage } from '../../services/app/message-type'; import { EventStatusEnum, RunAppRequest } from '../../services/app/type'; import { ToastModel } from '../../utils/toast.model'; +import { initBot } from '@/services/app'; @injectable() export class AppBuilderChatModel { @@ -137,18 +137,14 @@ export class AppBuilderChatModel { this.isInitBotLoading = true; }); try { - const res = await axios.post( - `/api/app/init_bot`, - { - automata, - }, - { headers: { 'Content-Type': 'application/json' } }, - ); - if (res.data.session_id == null && res.data.message) { - this.emitter.emitter.emit('message.error', res.data.message); + const reuslt = await initBot({ + automata, + }); + if (reuslt.data.session_id == null && reuslt.data.message) { + this.emitter.emitter.emit('message.error', reuslt.data.message); return; } - this.session_id = res.data.session_id; + this.session_id = reuslt.data.session_id; this.openRunDrawer(); await this.chatNew.isReadyPromise; this.chatNew.clearMemory(); diff --git a/web/apps/web/src/components/common/list-footer-extra/index.tsx b/web/apps/web/src/components/common/list-footer-extra/index.tsx index 4a211e24..32609c79 100644 --- a/web/apps/web/src/components/common/list-footer-extra/index.tsx +++ b/web/apps/web/src/components/common/list-footer-extra/index.tsx @@ -1,9 +1,9 @@ -import { IconButton, Setting } from '@shellagent/ui'; import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'; +import { IconButton, Setting } from '@shellagent/ui'; import { useInjection } from 'inversify-react'; -import { SettingsModel } from '@/components/settings/settings.model'; import { FeedbackModel } from '@/components/feedback/model'; +import { SettingsModel } from '@/components/settings/settings.model'; export const ListFooterExtra = () => { const settingsModel = useInjection(SettingsModel); @@ -13,8 +13,6 @@ export const ListFooterExtra = () => { process.env.NEXT_PUBLIC_DISABLE_SETTING === 'yes' || process.env.NEXT_PUBLIC_API_URL?.includes('34.239.158.245'); - console.log('settingsDisabled: ', process.env.NEXT_PUBLIC_API_URL); - return (
{ - const [itemType, setItemType] = useState('models'); - const model = useInjection(DownloadModel); - - useEffect(() => { - if (itemType === 'models' && model.drawer.isOpen) { - model.getCompletedModels(); - } - }, [itemType, model.drawer.isOpen]); - - return ( -
-
- {/* */} - -
-
- {model.completedModels.map((item, idx) => ( - <> - {item.filename} -
-
-
- Form {item.source} -
- - {item.finish_time} - -
-
- - ))} -
-
- ); -}); diff --git a/web/apps/web/src/components/download/download-definitions.ts b/web/apps/web/src/components/download/download-definitions.ts deleted file mode 100644 index 5ff7d5b2..00000000 --- a/web/apps/web/src/components/download/download-definitions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z } from 'zod'; - -// start-Download-Center-Models -export const url_download_center_list = - '/api/download-center/get_downloading_models'; - -export const DownloadingModelItem = z.object({ - model_id: z.string(), - model_url: z.string(), - cache_path: z.string(), - force: z.boolean().optional(), - filename: z.string(), - status: z.string().optional(), - error_status: z.literal('failed').optional(), -}); - -export const DownloadModelListRes = z.object({ - models: z.array(DownloadingModelItem), -}); - -// stop-Download-Center-Models - -// start-Model-Cancel -export const url_model_cancel = `/api/models/cancel_download`; -export const ModelCancelRequest = z.object({ - cache_path: z.string(), -}); -// stop-Model-Cancel diff --git a/web/apps/web/src/components/download/download.model.ts b/web/apps/web/src/components/download/download.model.ts deleted file mode 100644 index a78d4d93..00000000 --- a/web/apps/web/src/components/download/download.model.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { - EventSourceMessage, - fetchEventSource, -} from '@microsoft/fetch-event-source'; -import axios from 'axios'; -import { inject, injectable } from 'inversify'; -import { action, makeObservable, observable } from 'mobx'; -import { z } from 'zod'; - -import { - DownloadingModelItem, - DownloadModelListRes, - url_download_center_list, -} from '@/components/download/download-definitions'; -import { url_models_marketplace_install } from '@/components/manager/manager-definitions'; -import { ModalModel } from '@/utils/modal.model'; -import { ToastModel } from '@/utils/toast.model'; - -@injectable() -export class DownloadModel { - @observable downloadingModels: z.infer[] = []; - @observable completedModels: any[] = []; - @observable tab: 'downloading' | 'completed' = 'downloading'; - @observable progressMap = new Map(); - progressAbortCtrl = new AbortController(); - - constructor( - @inject(ToastModel) private emitter: ToastModel, - @inject(ModalModel) public drawer: ModalModel, - ) { - makeObservable(this); - } - - @action.bound - setTab(tab: 'downloading' | 'completed') { - this.tab = tab; - } - - async forceInstall(id: string) { - try { - await axios.post( - url_models_marketplace_install, - { id, force: true }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async getProgress() { - this.progressAbortCtrl.abort(); - this.progressAbortCtrl = new AbortController(); - fetchEventSource('/api/download-center/get_downloading_models_progress', { - method: 'POST', - mode: 'cors', - headers: { - method: 'POST', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({}), - signal: this.progressAbortCtrl.signal, - openWhenHidden: true, - onerror: err => { - this.emitter.emitter.emit('message.error', err.message); - }, - onmessage: (ev: EventSourceMessage) => { - if (ev.event === 'download_start') { - this.getDownloadingModels(); - } - if (ev.event === 'download_end') { - this.getDownloadingModels(); - } - if (ev.event === 'downloading') { - try { - const data = JSON.parse(ev.data); - this.progressMap.set(data.input.model_id, data.output.progress); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - }, - onopen: async () => { - // - }, - }); - } - - async getDownloadingModels() { - try { - const res = await axios.post( - url_download_center_list, - {}, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - this.downloadingModels = DownloadModelListRes.parse(res.data).models; - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async getCompletedModels() { - try { - const res = await axios.post( - '/api/download-center/get_completed_models', - {}, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - this.completedModels = res.data.models; - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async cancelDownload(cachePath: string) { - try { - await axios.post( - '/api/models/cancel_download', - { - cache_path: cachePath, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - this.emitter.emitter.emit('message.success', 'Successfully cancelled.'); - this.getDownloadingModels(); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } -} diff --git a/web/apps/web/src/components/download/downloading/index.tsx b/web/apps/web/src/components/download/downloading/index.tsx deleted file mode 100644 index 26eebb7e..00000000 --- a/web/apps/web/src/components/download/downloading/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { ArrowPathIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import { Button, Heading, IconButton, Spinner, Text } from '@shellagent/ui'; -import { useInjection } from 'inversify-react'; -import { observer } from 'mobx-react-lite'; -import React, { useEffect, useState } from 'react'; - -import { DownloadModel } from '@/components/download/download.model'; -import { cn } from '@/utils/cn'; - -export const Downloading = observer(() => { - const [itemType, setItemType] = useState('models'); - const model = useInjection(DownloadModel); - useEffect(() => { - if (itemType === 'models' && model.drawer.isOpen) { - model.getDownloadingModels(); - model.getProgress(); - } - }, [itemType, model.drawer.isOpen]); - - return ( -
-
- {/* */} - -
-
- {model.downloadingModels.map((item, idx) => { - const progress = model.progressMap.get(item.model_id) || 0; - return ( -
-
- {item.error_status === 'failed' ? ( - - ) : ( - - )} -
-
-
- {item.filename} - {item.error_status !== 'failed' ? ( - {`${progress}%`} - ) : null} -
-
- {item.error_status === 'failed' ? ( - model.forceInstall(item.model_id)} - /> - ) : null} - model.cancelDownload(item.cache_path)} - /> -
-
-
- ); - })} -
-
- ); -}); diff --git a/web/apps/web/src/components/download/index.tsx b/web/apps/web/src/components/download/index.tsx deleted file mode 100644 index 2733a087..00000000 --- a/web/apps/web/src/components/download/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ArrowDownTrayIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import { - Drawer, - IconButton, - Tabs, - TabsList, - TabsTrigger, -} from '@shellagent/ui'; -import { useInjection } from 'inversify-react'; -import { observer } from 'mobx-react-lite'; -import React from 'react'; - -import { DownloadModel } from '@/components/download/download.model'; - -import { Completed } from './completed'; -import { Downloading } from './downloading'; - -export const Download = observer(() => { - const model = useInjection(DownloadModel); - return ( -
- - - model.setTab(val as any)}> - - - Downloading - - - Completed - - - - -
- {model.tab === 'downloading' ? : } -
-
-
- ); -}); diff --git a/web/apps/web/src/components/manager/index.tsx b/web/apps/web/src/components/manager/index.tsx deleted file mode 100644 index dd27163f..00000000 --- a/web/apps/web/src/components/manager/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { ButtonProps, AModal } from '@shellagent/ui'; -import { Flex } from 'react-system'; - -import { useGlobalStore } from '@/stores/global/global-provider'; - -import { ManagerContent } from './manager-content'; -import { ManagerSideBar } from './manger-sider'; - -interface ManagerDialogProps { - buttonSize?: ButtonProps['size']; -} - -export const ManagerDialog: React.FC = () => { - const { managerDialogOpen, setManagerDialogOpen } = useGlobalStore(state => ({ - managerDialogOpen: state.managerDialogOpen, - setManagerDialogOpen: state.setManagerDialogOpen, - })); - - return ( - setManagerDialogOpen(false)} - footer={null}> - - - - - - ); -}; diff --git a/web/apps/web/src/components/manager/manager-content/index.tsx b/web/apps/web/src/components/manager/manager-content/index.tsx deleted file mode 100644 index 0bf6d818..00000000 --- a/web/apps/web/src/components/manager/manager-content/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** @jsxImportSource @emotion/react */ - -import { css } from '@emotion/react'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shellagent/ui'; - -import { - BaseTypeEnum, - ManagerTypeEnum, - useGlobalStore, -} from '@/stores/global/global-provider'; - -import { ModelsInstalled } from './models/models-installed'; -import { ModelsMarketplace } from './models/models-marketplace'; -import { WidgetsInstalled } from './widgets/widgets-installed'; -import { WidgetsMarketplace } from './widgets/widgets-marketplace'; - -export const ManagerContent = () => { - const { setBaseType, managerType } = useGlobalStore(state => ({ - setBaseType: state.setBaseType, - managerType: state.managerType, - })); - - return ( - setBaseType(value as BaseTypeEnum)}> - - - Marketplace - - - Installed - - - - {managerType === ManagerTypeEnum.widget ? ( - - ) : ( - - )} - - - {managerType === ManagerTypeEnum.widget ? ( - - ) : ( - - )} - - - ); -}; diff --git a/web/apps/web/src/components/manager/manager-content/manager-confg.ts b/web/apps/web/src/components/manager/manager-content/manager-confg.ts deleted file mode 100644 index 3dcc91a1..00000000 --- a/web/apps/web/src/components/manager/manager-content/manager-confg.ts +++ /dev/null @@ -1 +0,0 @@ -export const IS_MANAGER_PROD = true; diff --git a/web/apps/web/src/components/manager/manager-content/models/models-common.model.ts b/web/apps/web/src/components/manager/manager-content/models/models-common.model.ts deleted file mode 100644 index 0f9313d2..00000000 --- a/web/apps/web/src/components/manager/manager-content/models/models-common.model.ts +++ /dev/null @@ -1,164 +0,0 @@ -import axios from 'axios'; -import { FormikProps } from 'formik'; -import { inject, injectable } from 'inversify'; -import { debounce, DebouncedFunc } from 'lodash-es'; -import { action, makeObservable, observable, runInAction } from 'mobx'; - -import { url_models_marketplace_install } from '@/components/manager/manager-definitions'; -import { ModalModel } from '@/utils/modal.model'; - -import { ToastModel } from '../../../../utils/toast.model'; - -@injectable() -export class ModelsCommonModel { - formikProps: FormikProps | undefined; - @observable types = []; - @observable bases = []; - @observable save_paths = []; - @observable installLoadingMap = new Map(); - @observable uninstallLoadingMap = new Map(); - @observable checkedType = 'All'; - @observable checkedBase = 'All'; - @observable checkedSavePath = 'All'; - debouneFunc: DebouncedFunc; - - constructor( - @inject(ToastModel) private emitter: ToastModel, - @inject(ModalModel) public installFromModal: ModalModel, - ) { - makeObservable(this); - this.debouneFunc = debounce( - () => { - this.emitter.emitter.emit( - 'message.success', - 'Add to installed queue. Please see the progress in download center', - ); - }, - 5000, - { - leading: true, - trailing: false, - }, - ); - } - - @action.bound - setCheckedType(val: string) { - this.checkedType = val; - } - - @action.bound - setCheckedSavePath(val: string) { - this.checkedSavePath = val; - } - - @action.bound - setCheckedBase(val: string) { - this.checkedBase = val; - } - - async loadTypeBaseList() { - try { - const res = await axios.post( - '/api/models/marketplace/types_bases_lists', - {}, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - runInAction(() => { - this.types = res.data.types; - this.bases = res.data.bases; - this.save_paths = res.data.save_paths; - }); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async installFromLink(values: any) { - try { - await axios.post('/api/models/marketplace/install_from_link', values, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.emitter.emitter.emit( - 'message.success', - 'Add to installed queue. Please see the progress in download center', - ); - this.installFromModal.close(); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async batchInstall(ids: string[], loadData: () => Promise) { - ids.forEach(id => this.install(id, loadData)); - } - - async install(id: string, loadData: () => Promise) { - try { - this.installLoadingMap.set(id, true); - try { - await axios.post( - url_models_marketplace_install, - { id }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - loadData(); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } finally { - this.installLoadingMap.delete(id); - } - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - async unInstall(idList: string[], loadData: () => Promise) { - try { - idList.forEach(id => { - runInAction(() => { - this.uninstallLoadingMap.set(id, true); - }); - }); - await axios.post( - '/api/models/marketplace/batch_uninstall', - { - id_list: idList, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - this.emitter.emitter.emit('message.success', 'Successfully uninstalled'); - loadData(); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } finally { - idList.forEach(id => { - runInAction(() => { - this.uninstallLoadingMap.delete(id); - }); - }); - } - } - - async onSubmit() { - try { - await this.formikProps?.submitForm(); - } catch (error) { - // - } - } -} diff --git a/web/apps/web/src/components/manager/manager-content/models/models-installed.model.ts b/web/apps/web/src/components/manager/manager-content/models/models-installed.model.ts deleted file mode 100644 index a8fc886e..00000000 --- a/web/apps/web/src/components/manager/manager-content/models/models-installed.model.ts +++ /dev/null @@ -1,97 +0,0 @@ -import axios from 'axios'; -import { inject, injectable } from 'inversify'; -import { action, makeObservable, observable, runInAction } from 'mobx'; - -import { url_models_installed_list } from '@/components/manager/manager-definitions'; -import { SettingsModel } from '@/components/settings/settings.model'; -import { ToastModel } from '@/utils/toast.model'; - -import { ModelsCommonModel } from './models-common.model'; - -@injectable() -export class ModelsInstalledModel { - @observable data: any[] = []; - @observable keyword: string = ''; - @observable loadingData: boolean = false; - - constructor( - @inject(ModelsCommonModel) public common: ModelsCommonModel, - @inject(ToastModel) public emitter: ToastModel, - @inject(SettingsModel) private settings: SettingsModel, - ) { - makeObservable(this); - } - - @action.bound - setCheckedType(val: string) { - this.common.setCheckedType(val); - this.loadData(); - } - - @action.bound - setCheckedBase(val: string) { - this.common.setCheckedBase(val); - this.loadData(); - } - - async loadData() { - runInAction(() => { - this.loadingData = true; - }); - const settingsDisabled = process.env.NEXT_PUBLIC_DISABLE_SETTING === 'yes'; - let location; - if (settingsDisabled) { - location = 'models'; - } else { - const settingsEnv = await this.settings.loadSettingsEnv(); - location = settingsEnv?.model_location; - } - - if (location == null) { - this.emitter.emitter.emit( - 'message.error', - `model_location is null ${ - settingsDisabled ? '' : ' ,go to Settings to set a value' - }`, - ); - } - try { - const res = await axios.post( - url_models_installed_list, - { - base: - this.common.checkedBase === 'All' ? '' : this.common.checkedBase, - save_path: - this.common.checkedSavePath === 'All' - ? '' - : this.common.checkedSavePath, - query: this.keyword, - location, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (res?.data?.success) { - runInAction(() => { - this.data = res.data.data; - }); - } else { - this.emitter.emitter.emit('message.error', `load data error`); - } - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } finally { - runInAction(() => { - this.loadingData = false; - }); - } - } - - @action.bound - changeKeyword(keyword: string) { - this.keyword = keyword; - } -} diff --git a/web/apps/web/src/components/manager/manager-content/models/models-installed.tsx b/web/apps/web/src/components/manager/manager-content/models/models-installed.tsx deleted file mode 100644 index 82714ce6..00000000 --- a/web/apps/web/src/components/manager/manager-content/models/models-installed.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { SearchOutlined } from '@ant-design/icons'; -import { css } from '@emotion/react'; -import { AInput, AModal, ATable } from '@shellagent/ui'; -import { Tag, Typography } from 'antd'; -import { ColumnsType } from 'antd/es/table'; -import { useInjection } from 'inversify-react'; -import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import { Box, Flex } from 'react-system'; - -import { useGlobalStore } from '@/stores/global/global-provider'; - -import { ModelsCommonModel } from './models-common.model'; -import { ModelsInstalledModel } from './models-installed.model'; -import { - InstallFromLinkModalContent, - ModelsActionButton, - TopInstallButton, -} from './models-marketplace'; - -const ModelsSavePathBase = observer<{ - loadData: () => Promise; -}>(props => { - const model = useInjection(ModelsCommonModel); - return ( -
- { - return expanded ? 'Collapse' : 'More'; - }, - }}> - - Save Path: - - { - model.setCheckedSavePath('All'); - props.loadData(); - }}> - All - - {model.save_paths.map(type => ( - { - model.setCheckedSavePath(type); - props.loadData(); - }}> - {type} - - ))} - - - { - return expanded ? 'Collapse' : 'More'; - }, - }}> - - Base: - - { - model.setCheckedBase('All'); - props.loadData(); - }}> - All - - {model.bases.map(type => ( - { - model.setCheckedBase(type); - props.loadData(); - }}> - {type} - - ))} - -
- ); -}); - -export const ModelsInstalled = observer<{}>(() => { - const model = useInjection(ModelsInstalledModel); - const { managerDialogOpen } = useGlobalStore(state => ({ - managerDialogOpen: state.managerDialogOpen, - })); - useEffect(() => { - if (managerDialogOpen) { - model.loadData(); - model.common.loadTypeBaseList(); - } - }, [managerDialogOpen]); - const columns: ColumnsType = [ - { - title: 'Filename', - dataIndex: 'filename', - }, - { - title: 'Save Path', - dataIndex: 'save_path', - render: text => { - return {text}; - }, - }, - { - title: 'Local Path', - dataIndex: 'local_path', - }, - { - title: 'Action', - align: 'center', - fixed: 'right', - dataIndex: 'action', - render: (text, record) => { - return ( - model.loadData()} - /> - ); - }, - }, - ]; - return ( - <> - {/* - Location: {model.location} - */} - - - } - placeholder="Search MyShell..." - value={model.keyword} - onChange={e => model.changeKeyword(e.target.value)} - onPressEnter={() => model.loadData()} - /> - -
- model.loadData()} /> -
-
- model.loadData()} /> - - model.common.onSubmit()}> - - - - ); -}); diff --git a/web/apps/web/src/components/manager/manager-content/models/models-marketplace.model.ts b/web/apps/web/src/components/manager/manager-content/models/models-marketplace.model.ts deleted file mode 100644 index c28f3919..00000000 --- a/web/apps/web/src/components/manager/manager-content/models/models-marketplace.model.ts +++ /dev/null @@ -1,85 +0,0 @@ -import axios from 'axios'; -import { inject, injectable } from 'inversify'; -import { action, computed, makeObservable, observable } from 'mobx'; -import { z } from 'zod'; - -import { - ModelsMarketPlaceItem, - ModelsMarketPlaceRes, - url_models_marketplace_list, -} from '@/components/manager/manager-definitions'; -import { RequestModel } from '@/utils/request.model'; -import { ToastModel } from '@/utils/toast.model'; - -import { ModelsCommonModel } from './models-common.model'; - -@injectable() -export class ModelsMarketplaceModel { - @observable data: z.infer[] = []; - @observable keyword: string = ''; - @observable selectedIds: string[] = []; - - constructor( - @inject(ToastModel) private emitter: ToastModel, - @inject(RequestModel) public loadData: RequestModel, - @inject(ModelsCommonModel) public common: ModelsCommonModel, - ) { - makeObservable(this); - this.loadData.handlers = { - requestFunc: (requestId, abortController) => { - return axios.post(url_models_marketplace_list, this.requestBody, { - signal: abortController.signal, - headers: { - 'Content-Type': 'application/json', - }, - }); - }, - onsuccess: data => { - const result = ModelsMarketPlaceRes.parse(data); - if (result.success) { - this.data = result.data; - } else { - this.emitter.emitter.emit('message.error', result.message); - } - }, - onerror: e => { - this.emitter.emitter.emit('message.error', e.message); - }, - }; - } - - @computed get requestBody() { - return { - base: this.common.checkedBase === 'All' ? '' : this.common.checkedBase, - type: this.common.checkedType === 'All' ? '' : this.common.checkedType, - query: this.keyword, - }; - } - - @action.bound - setCheckedType(val: string) { - this.common.setCheckedType(val); - this.loadData.request(); - } - - @action.bound - setCheckedBase(val: string) { - this.common.setCheckedBase(val); - this.loadData.request(); - } - - @action.bound - changeKeyword(keyword: string) { - this.keyword = keyword; - } - - @action.bound - isRowCheckboxDisabled(record: any) { - return record.install_status !== 'not_installed'; - } - - @action.bound - setSelectedIds(ids: string[]) { - this.selectedIds = ids; - } -} diff --git a/web/apps/web/src/components/manager/manager-content/models/models-marketplace.tsx b/web/apps/web/src/components/manager/manager-content/models/models-marketplace.tsx deleted file mode 100644 index cd191bc2..00000000 --- a/web/apps/web/src/components/manager/manager-content/models/models-marketplace.tsx +++ /dev/null @@ -1,405 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { DownOutlined, SearchOutlined } from '@ant-design/icons'; -import { css } from '@emotion/react'; -import { - AButton, - ADropdownButton, - AInput, - AModal, - ATable, -} from '@shellagent/ui'; -import { Form, Input, Popconfirm, Select, Tag, Typography } from 'antd'; -import { ColumnsType } from 'antd/es/table'; -import { Field, FieldProps, Formik } from 'formik'; -import { useInjection } from 'inversify-react'; -import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import { Box, Flex } from 'react-system'; -import { z } from 'zod'; - -import { ModelsMarketPlaceItem } from '@/components/manager/manager-definitions'; -import { useGlobalStore } from '@/stores/global/global-provider'; - -import { ModelsCommonModel } from './models-common.model'; -import { ModelsMarketplaceModel } from './models-marketplace.model'; - -export const ModelsTypeBase = observer<{ - loadData: () => Promise; -}>(props => { - const model = useInjection(ModelsCommonModel); - return ( -
- { - return expanded ? 'Collapse' : 'More'; - }, - }}> - - Type: - - { - model.setCheckedType('All'); - props.loadData(); - }}> - All - - {model.types.map(type => ( - { - model.setCheckedType(type); - props.loadData(); - }}> - {type} - - ))} - - - { - return expanded ? 'Collapse' : 'More'; - }, - }}> - - Base: - - { - model.setCheckedBase('All'); - props.loadData(); - }}> - All - - {model.bases.map(type => ( - { - model.setCheckedBase(type); - props.loadData(); - }}> - {type} - - ))} - -
- ); -}); - -export const ModelsActionButton = observer<{ - record: z.infer; - loadData: () => Promise; -}>(props => { - const model = useInjection(ModelsCommonModel); - const installLoading = model.installLoadingMap.get(props.record.id); - const uninstallLoading = model.uninstallLoadingMap.get(props.record.id); - return ( -
- {props.record.install_status === 'not_installed' ? ( - - model.install(props.record.id, () => props.loadData()) - }> - Install - - ) : null} - {props.record.install_status === 'installing' ? ( - - Installing - - ) : null} - {props.record.install_status === 'installed' ? ( - - { - model.unInstall([props.record.id], () => props.loadData()); - }}> - - Uninstall - - - - ) : null} -
- ); -}); - -export const TopInstallButton = observer<{ - selectedIds?: string[]; - loadData: () => Promise; -}>(props => { - const model = useInjection(ModelsCommonModel); - if (props.selectedIds == null) { - return ( - Install from Link - ); - } - return ( - { - model.batchInstall(props.selectedIds!, props.loadData); - }} - css={css` - .ant-btn { - color: var(--ant-color-primary); - } - `} - icon={} - menu={{ - onClick: e => { - if (e.key === 'install-from-link') { - model.installFromModal.open(); - } - }, - items: [ - { - key: 'install-from-link', - label: 'Install from Link', - }, - ], - }}> - Install - {props.selectedIds.length > 0 ? ` (${props.selectedIds.length})` : ''} - - ); -}); - -export const InstallFromLinkModalContent = observer(() => { - const model = useInjection(ModelsCommonModel); - return ( - { - model.installFromLink(values); - }}> - {formikProps => { - model.formikProps = formikProps; - return ( -
- - {({ field }: FieldProps) => ( - - - - )} - - - {({ field, form }: FieldProps) => { - return ( - - - - )} - -
- ); - }} -
- ); -}); - -export const ModelsMarketplace = observer<{}>(() => { - const model = useInjection(ModelsMarketplaceModel); - const { managerDialogOpen } = useGlobalStore(state => ({ - managerDialogOpen: state.managerDialogOpen, - })); - useEffect(() => { - if (managerDialogOpen) { - model.loadData.request(); - model.common.loadTypeBaseList(); - } - }, [managerDialogOpen]); - - const columns: ColumnsType> = [ - { - title: 'Name', - dataIndex: 'name', - render: (text, record) => { - return ( - - {text} - - ); - }, - }, - { - title: 'Description', - dataIndex: 'description', - render: text => { - return ( - - {text} - - ); - }, - }, - { - title: 'Type', - dataIndex: 'type', - render: text => { - return {text}; - }, - }, - { - title: 'Base', - dataIndex: 'base', - render: text => { - return {text}; - }, - }, - { - title: 'Action', - align: 'center', - fixed: 'right', - dataIndex: 'action', - render: (text, record) => { - return ( - model.loadData.request()} - /> - ); - }, - }, - ]; - return ( - <> - - - } - placeholder="Search MyShell..." - value={model.keyword} - onChange={e => model.changeKeyword(e.target.value)} - onPressEnter={() => model.loadData.request()} - /> - -
- model.loadData.request()} - /> -
-
- model.loadData.request()} /> - { - model.setSelectedIds(selectedRows.map(item => item.id)); - }, - onSelectAll: (selected, selectedRows) => { - model.setSelectedIds(selectedRows.map(item => item.id)); - }, - }} - loading={model.loadData.loading} - scroll={{ y: 320 }} - rowKey="id" - pagination={{ - position: ['bottomLeft'], - simple: true, - defaultCurrent: 1, - total: model.data.length, - showSizeChanger: false, - }} - dataSource={model.data} - columns={columns as any} - /> - model.common.onSubmit()}> - - - - ); -}); diff --git a/web/apps/web/src/components/manager/manager-content/widgets/widgets-common.model.ts b/web/apps/web/src/components/manager/manager-content/widgets/widgets-common.model.ts deleted file mode 100644 index ea32d48b..00000000 --- a/web/apps/web/src/components/manager/manager-content/widgets/widgets-common.model.ts +++ /dev/null @@ -1,246 +0,0 @@ -import axios from 'axios'; -import type { FormikProps } from 'formik'; -import { inject, injectable } from 'inversify'; -import { - action, - computed, - makeObservable, - observable, - runInAction, -} from 'mobx'; - -import { ToastModel } from '../../../../utils/toast.model'; - -export type WidgetRecord = { - local_commits_hash?: string[]; - remote_commit_hash: string; - current_commit: string; -}; - -export function getInstallStatus( - record: WidgetRecord, -): 'Install' | 'Update' | null { - if (!record) return null; - if (record.local_commits_hash?.indexOf(record.remote_commit_hash) === -1) { - return 'Update'; - } - return null; -} - -@injectable() -export class WidgetsCommonModel { - formikProps: FormikProps | undefined; - @observable installFromModalVisible = false; - @observable insatallFromGitLoading = false; - @observable installUpdateLoadingMap = new Map(); - @observable uninstallLoadingMap = new Map(); - @observable remoteVersionData: { - tags: Record; - commits: Record< - string, - { - author: string; - date: string; - message: string; - } - >; - } = { - tags: {}, - commits: {}, - }; - @observable remoteVersionQuery: string = ''; - - constructor(@inject(ToastModel) private emitter: ToastModel) { - makeObservable(this); - } - - @computed get remoteList() { - const tags = Object.keys(this.remoteVersionData.tags); - const commits = Object.keys(this.remoteVersionData.commits); - return [...tags, ...commits]; - } - - @action.bound - onChangeRemoteVersionQuery(val: string) { - this.remoteVersionQuery = val; - } - - onRemoteVersionOpenChange(open: boolean, git: string) { - if (open) { - this.loadRemoteVersions(git, this.remoteVersionQuery); - } else { - this.remoteVersionQuery = ''; - } - } - - async onInstallFromGitSubmit() { - try { - await this.formikProps?.submitForm(); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - @action.bound - openInstallFromGitModal() { - this.installFromModalVisible = true; - } - - @action.bound - closeInstallFromGitModal() { - this.installFromModalVisible = false; - } - - async installFromGit(values: any) { - runInAction(() => { - this.insatallFromGitLoading = true; - }); - try { - await axios.post('/api/widgets/marketplace/install_from_git', values, { - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - runInAction(() => { - this.insatallFromGitLoading = false; - }); - } - - async loadRemoteVersions(git: string, query: string = '') { - this.remoteVersionData = { - tags: {}, - commits: {}, - }; - try { - const res = await axios.post( - '/api/widgets/get_remote_versions', - { - git, - query, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (res?.data?.success) { - runInAction(() => { - this.remoteVersionData = res.data; - }); - } else { - this.emitter.emitter.emit('message.error', res.data.message); - } - } catch (e: any) { - this.emitter.emitter.emit('message.error', e.message); - } - } - - // eslint-disable-next-line class-methods-use-this - showUninstall(record: any): boolean { - return record?.local_commits_hash?.length > 0; - } - - getInstallStatus(record: any) { - return getInstallStatus(record); - } - - async installVersion( - id: string, - commit: string, - loadData: () => Promise, - ) { - if (this.remoteVersionData.tags[commit]) { - commit = this.remoteVersionData.tags[commit]; - } - runInAction(() => { - this.installUpdateLoadingMap.set(id, true); - }); - try { - await axios.post( - '/api/widgets/marketplace/install', - { - id, - commit, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - runInAction(() => { - this.installUpdateLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.success', 'Successfully installed'); - loadData(); - } catch (e: any) { - runInAction(() => { - this.installUpdateLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.error', e.message); - } - } - - async unInstallVersion( - id: string, - commit: string | undefined, - loadData: () => Promise, - ) { - runInAction(() => { - this.uninstallLoadingMap.set(id, true); - }); - try { - await axios.post( - '/api/widgets/marketplace/uninstall', - { - id, - commit, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - runInAction(() => { - this.uninstallLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.success', 'Successfully uninstalled'); - loadData(); - } catch (e: any) { - runInAction(() => { - this.uninstallLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.error', e.message); - } - } - - canUpdate( - commit: string, - record: WidgetRecord, - ): 'can_update' | 'installed' | 'current' { - commit = this.tagToCommit(commit); - if (commit === record.current_commit) return 'current'; - if ( - Array.isArray(record.local_commits_hash) && - record.local_commits_hash.indexOf(commit) > -1 - ) - return 'installed'; - return 'can_update'; - } - - /** - * 根据获取的 remoteVersionData 将 tag 转成 commit hash - * @param commit 可能是 tag 形式 v1 - */ - tagToCommit(commit: string) { - if (this.remoteVersionData.tags[commit]) { - commit = this.remoteVersionData.tags[commit]; - } - return commit; - } -} diff --git a/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.model.ts b/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.model.ts deleted file mode 100644 index 515b49b4..00000000 --- a/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.model.ts +++ /dev/null @@ -1,154 +0,0 @@ -import axios from 'axios'; -import { inject, injectable } from 'inversify'; -import { action, makeObservable, observable, runInAction } from 'mobx'; - -import { WidgetsCommonModel } from './widgets-common.model'; -import { ToastModel } from '../../../../utils/toast.model'; - -export type ActionKey = 'Install' | 'Update' | 'Uninstall'; - -@injectable() -export class WidgetsInstalledModel { - @observable data: any[] = []; - @observable keyword: string = ''; - @observable enableLoadingMap = new Map(); - - constructor( - @inject(WidgetsCommonModel) public common: WidgetsCommonModel, - @inject(ToastModel) private emitter: ToastModel, - ) { - makeObservable(this); - } - - async loadData() { - const res = await axios.post( - '/api/widgets/installed/list', - { - query: this.keyword.trim(), - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (res?.data?.success) { - runInAction(() => { - this.data = res.data.data; - }); - } - } - - isDisabled(id: string, type: 'install' | 'uninstall' | 'enable') { - if (type === 'install') { - return ( - this.common.uninstallLoadingMap.has(id) || this.enableLoadingMap.has(id) - ); - } - if (type === 'uninstall') { - return ( - this.common.installUpdateLoadingMap.has(id) || - this.enableLoadingMap.has(id) - ); - } - if (type === 'enable') { - return ( - this.common.uninstallLoadingMap.has(id) || - this.common.installUpdateLoadingMap.has(id) - ); - } - return false; - } - - async setEnableStatus(id: string, enabled: boolean) { - runInAction(() => { - this.enableLoadingMap.set(id, true); - }); - try { - await axios.post( - '/api/widgets/installed/set_widget_status', - { - id, - enabled, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - this.emitter.emitter.emit( - 'message.success', - enabled ? 'Successfully enabled' : 'Successfully disabled', - ); - runInAction(() => { - this.enableLoadingMap.delete(id); - }); - this.loadData(); - } catch (e: any) { - runInAction(() => { - this.enableLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.error', e.message); - } - } - - async setCurrent(id: string, current_commit: string) { - runInAction(() => { - this.common.installUpdateLoadingMap.set(id, true); - }); - try { - await axios.post( - '/api/widgets/installed/set_widget_status', - { - id, - current_commit, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - runInAction(() => { - this.common.installUpdateLoadingMap.delete(id); - }); - this.emitter.emitter.emit( - 'message.success', - 'Successfully set as current version', - ); - this.loadData(); - } catch (e: any) { - runInAction(() => { - this.common.installUpdateLoadingMap.delete(id); - }); - this.emitter.emitter.emit('message.error', e.message); - } - } - - @action.bound - changeKeyword(keyword: string) { - this.keyword = keyword; - } - - canSetCurrent(record: any, item: string) { - return this.isInstalled(record, item) && !this.isCurrent(record, item); - } - - isInstalled(record: any, item: string) { - let commit = item; - if (this.common.remoteVersionData.tags[item]) { - commit = this.common.remoteVersionData.tags[item]; - } - const installed = record?.local_commits_hash?.includes(commit); - return installed; - } - - isCurrent(record: any, item: string) { - let commit = item; - if (this.common.remoteVersionData.tags[item]) { - commit = this.common.remoteVersionData.tags[item]; - } - return commit === record.current_commit; - } -} diff --git a/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.tsx b/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.tsx deleted file mode 100644 index 110ae156..00000000 --- a/web/apps/web/src/components/manager/manager-content/widgets/widgets-installed.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { SearchOutlined } from '@ant-design/icons'; -import { AInput, ATable } from '@shellagent/ui'; -import { Switch, Typography } from 'antd'; -import { ColumnsType } from 'antd/es/table'; -import { useInjection } from 'inversify-react'; -import { trace } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import { Box, Flex } from 'react-system'; - -import { useGlobalStore } from '@/stores/global/global-provider'; - -import { WidgetsInstalledModel } from './widgets-installed.model'; -import { - InstallFromGitModal, - TopInstallButton, - WidgetsActionButton, -} from './widgets-marketplace'; - -export const WidgetsStatus = observer<{ - record: any; -}>(props => { - trace(); - const model = useInjection(WidgetsInstalledModel); - return ( - { - model.setEnableStatus(props.record.id, e); - }} - /> - ); -}); - -export const WidgetsInstalled = observer(() => { - trace(); - const model = useInjection(WidgetsInstalledModel); - - const { managerDialogOpen } = useGlobalStore(state => ({ - managerDialogOpen: state.managerDialogOpen, - })); - useEffect(() => { - if (managerDialogOpen) { - model.loadData(); - } - }, [managerDialogOpen]); - - const columns: ColumnsType = [ - { - title: 'Name', - dataIndex: 'name', - render: (text, record) => { - return ( - - {text} - - ); - }, - }, - { - title: 'Description', - dataIndex: 'description', - render: text => { - return ( - - {text} - - ); - }, - }, - { - title: 'Enable/Disable', - dataIndex: 'status', - render: (text, record) => { - return ; - }, - }, - { - title: 'Action', - align: 'center', - width: 1, - dataIndex: 'action', - render: (text, record) => { - return ( - model.loadData()} - /> - ); - }, - }, - ]; - return ( - <> - - - } - placeholder="Search MyShell..." - value={model.keyword} - onChange={e => model.changeKeyword(e.target.value)} - onPressEnter={() => model.loadData()} - /> - -
- -
-
- - - - - - ); -}); diff --git a/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.model.ts b/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.model.ts deleted file mode 100644 index 9cf8b927..00000000 --- a/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -import axios from 'axios'; -import { inject, injectable } from 'inversify'; -import { action, makeObservable, observable, runInAction } from 'mobx'; - -import { WidgetsCommonModel } from './widgets-common.model'; - -export type ActionKey = 'Install' | 'Update' | 'Uninstall'; - -@injectable() -export class WidgetsMarketplaceModel { - constructor(@inject(WidgetsCommonModel) public common: WidgetsCommonModel) { - makeObservable(this); - } - - @observable data: any[] = []; - - @observable keyword: string = ''; - - async loadData() { - const res = await axios.post( - '/api/widgets/marketplace/list', - { - query: this.keyword.trim(), - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (res?.data?.success) { - runInAction(() => { - this.data = res.data.data; - }); - } - } - - @action.bound - changeKeyword(keyword: string) { - this.keyword = keyword; - } -} diff --git a/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.tsx b/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.tsx deleted file mode 100644 index eab3600a..00000000 --- a/web/apps/web/src/components/manager/manager-content/widgets/widgets-marketplace.tsx +++ /dev/null @@ -1,396 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { - CheckOutlined, - CloseOutlined, - DownOutlined, - SearchOutlined, -} from '@ant-design/icons'; -import { css } from '@emotion/react'; -import { - AButton, - ADropdownButton, - AInput, - AModal, - ATable, -} from '@shellagent/ui'; -import { - Dropdown, - Form, - Input, - Popconfirm, - theme, - Tooltip, - Typography, -} from 'antd'; -import { ColumnsType } from 'antd/es/table'; -import { Field, FieldProps, Formik } from 'formik'; -import { useInjection } from 'inversify-react'; -import { trace } from 'mobx'; -import { observer } from 'mobx-react-lite'; -import { useEffect } from 'react'; -import { Box, Flex } from 'react-system'; - -import { useGlobalStore } from '@/stores/global/global-provider'; - -import { WidgetsCommonModel } from './widgets-common.model'; -import { WidgetsMarketplaceModel } from './widgets-marketplace.model'; - -export const InstallFromGitModalContent = observer<{ - model: WidgetsCommonModel; -}>(props => { - return ( - { - await props.model.installFromGit(values); - }}> - {formikProps => { - props.model.formikProps = formikProps; - return ( -
- - {({ field }: FieldProps) => ( - - - - )} - - - {({ field }: FieldProps) => ( - - - - )} - -
- ); - }} -
- ); -}); - -export const InstallFromGitModal = observer<{ - model: WidgetsCommonModel; -}>(props => { - trace(); - return ( - props.model.onInstallFromGitSubmit()}> - - - ); -}); - -export const VersionCommitSearchInput = observer<{ - model: WidgetsCommonModel; - record: any; -}>(props => { - return ( - { - e.preventDefault(); - e.stopPropagation(); - props.model.onChangeRemoteVersionQuery(e.target.value); - }} - onPressEnter={() => { - props.model.loadRemoteVersions( - props.record.git, - props.model.remoteVersionQuery, - ); - }} - prefix={} - placeholder="Search versions/commits ..." - /> - ); -}); - -export const WidgetsActionButton = observer<{ - record: any; - model: WidgetsCommonModel; - loadData: () => Promise; -}>(props => { - const installLoading = props.model.installUpdateLoadingMap.get( - props.record.id, - ); - const uninstallLoading = props.model.uninstallLoadingMap.get(props.record.id); - const { token } = theme.useToken(); - - const contentStyle: React.CSSProperties = { - backgroundColor: token.colorBgElevated, - borderRadius: token.borderRadiusLG, - boxShadow: token.boxShadowSecondary, - }; - - const installItems = props.model.remoteList.map((item: string) => { - const canUpdate = props.model.canUpdate(item, props.record); - const label = ( - - - {item}{' '} - {canUpdate === 'can_update' ? ( - - (click to update){' '} - - ) : null} - {canUpdate === 'current' ? ( - (current) - ) : null} - - {canUpdate === 'current' ? ( - - ) : null} - {canUpdate === 'installed' ? ( - - ) : null} - - ); - return { - key: item, - disabled: canUpdate === 'current', - label: - canUpdate === 'installed' ? ( - - {label} - - ) : ( - label - ), - }; - }); - - return ( -
- { - props.model.onRemoteVersionOpenChange(open, props.record.git); - }} - menu={{ - selectedKeys: [], - items: installItems, - onClick: e => { - props.model.installVersion(props.record.id, e.key, props.loadData); - }, - }} - dropdownRender={menu => { - return ( -
- - - Install Another Version - - - - - - {menu} -
- ); - }}> - - - - {props.model.getInstallStatus(props.record) || 'Install'} - - {!installLoading && } - - -
- {props.model.showUninstall(props.record) && ( - - { - props.model.unInstallVersion( - props.record.id, - undefined, - props.loadData, - ); - }}> - } - loading={uninstallLoading} - disabled={installLoading} - menu={{ - items: props.record?.local_commits_hash?.map((item: string) => { - return { - key: item, - label: ( - - { - props.model.unInstallVersion( - props.record.id, - item, - props.loadData, - ); - }} - title="Uninstall selected version" - description={`Are you sure to uninstall version ${item}?`}> - - {item} - - - - - ), - }; - }), - }}> - Uninstall - - - - )} -
- ); -}); - -export const TopInstallButton = observer<{ - model: WidgetsCommonModel; -}>(props => { - return ( - - Install from Git - - ); -}); - -export const WidgetsMarketplace = observer(() => { - const model = useInjection(WidgetsMarketplaceModel); - const { managerDialogOpen } = useGlobalStore(state => ({ - managerDialogOpen: state.managerDialogOpen, - })); - useEffect(() => { - if (managerDialogOpen) { - model.loadData(); - } - }, [managerDialogOpen]); - const columns: ColumnsType = [ - { - title: 'Name', - dataIndex: 'name', - render: (text, record) => { - return ( - - {text} - - ); - }, - }, - { - title: 'Description', - dataIndex: 'description', - render: text => { - return ( - - {text} - - ); - }, - }, - { - title: 'Action', - align: 'center', - width: 1, - dataIndex: 'action', - render: (text, record) => { - return ( - model.loadData()} - /> - ); - }, - }, - ]; - return ( - <> - - - } - placeholder="Search MyShell..." - value={model.keyword} - onChange={e => model.changeKeyword(e.target.value)} - onPressEnter={() => model.loadData()} - /> - -
- -
-
- - - - - - ); -}); diff --git a/web/apps/web/src/components/manager/manager-definitions.ts b/web/apps/web/src/components/manager/manager-definitions.ts deleted file mode 100644 index 3b99aa30..00000000 --- a/web/apps/web/src/components/manager/manager-definitions.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { z } from 'zod'; - -// start-url_types_bases_lists -export const url_types_bases_lists = - '/api/models/marketplace/types_bases_lists'; -// stop-url_types_bases_lists - -// start-ModelsMarketPlace -export const url_models_marketplace_list = '/api/models/marketplace/list'; - -export const ModelsMarketPlaceItem = z.object({ - id: z.string(), - name: z.string(), - type: z.string(), - base: z.string(), - link: z.array( - z.object({ - url: z.string().describe('作为链接, 后端返回了其他数据先不做校验'), - }), - ), - filename: z.string(), - description: z.string(), - install_status: z.union([ - z.literal('not_installed'), - z.literal('installed'), - z.literal('installing'), - ]), -}); -export const ModelsMarketPlaceSuccessRes = z.object({ - success: z.literal(true), - data: z.array(ModelsMarketPlaceItem), -}); - -export const ModelsMarketPlaceFailureRes = z.object({ - success: z.literal(false), - message: z.string(), -}); - -export const ModelsMarketPlaceRes = z.union([ - ModelsMarketPlaceSuccessRes, - ModelsMarketPlaceFailureRes, -]); -// stop-ModelsMarketPlace - -// start-ModelsMarketPlaceInstall -export const url_models_marketplace_install = '/api/models/marketplace/install'; -export const ModelsInstallRequest = z.object({ - id: z.string(), - force: z.literal('True').optional(), -}); - -export const downloading_event = z.object({ - output: z.object({ - progress: z.number(), - }), -}); - -// stop-ModelsMarketPlaceInstall - -// start-Manager-Models-Installed-List -export const url_models_installed_list = '/api/models/installed/list'; -export const ModelInstalledResItem = z.object({ - id: z.string(), - filename: z.string(), - save_path: z.string(), - local_path: z.string(), - create_time: z.coerce.date(), - update_time: z.coerce.date(), -}); -// stop-Manager-Models-Installed-List diff --git a/web/apps/web/src/components/manager/manger-sider/index.tsx b/web/apps/web/src/components/manager/manger-sider/index.tsx deleted file mode 100644 index 970c39a7..00000000 --- a/web/apps/web/src/components/manager/manger-sider/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { Heading, Tabs, TabsList, TabsTrigger } from '@shellagent/ui'; - -import { - useGlobalStore, - ManagerTypeEnum, -} from '@/stores/global/global-provider'; - -export const ManagerSideBar = () => { - const { managerType, setManagerType } = useGlobalStore(state => ({ - managerType: state.managerType, - setManagerType: state.setManagerType, - })); - - return ( -
- - Manager - - setManagerType(value as ManagerTypeEnum)} - className="w-full h-full p-0"> - - - Widgets - - - Models - - - -
- ); -}; diff --git a/web/apps/web/src/components/settings/settings.model.ts b/web/apps/web/src/components/settings/settings.model.ts index ae984573..a2d66ea4 100644 --- a/web/apps/web/src/components/settings/settings.model.ts +++ b/web/apps/web/src/components/settings/settings.model.ts @@ -1,4 +1,3 @@ -import axios from 'axios'; import { inject, injectable, postConstruct } from 'inversify'; import { action, makeObservable, observable } from 'mobx'; @@ -9,10 +8,21 @@ import { ToastModel } from '@/utils/toast.model'; import { DefaultEnvs, DefaultEnvsMap, - loadSettingEnvFormUrl, - saveSettingEnvFormUrl, SettingEnvFormValue, } from './settings-definitions'; +import { + autoUpdateSvc, + checkRepoStatusSvc, + getAutoUpdateSvc, + getCheckRepoStatusSvc, + getLastChecktimeSvc, + getUpdateChannelSvc, + loadEnvSvc, + restartSvc, + saveEnvSvc, + updateChannelSvc, + updateSvc, +} from '@/services/app'; export type SidebarValue = 'Environment' | 'SoftwareUpdate'; @@ -104,12 +114,8 @@ export class SettingsModel { @action.bound async getLastChecktime() { try { - const res = await axios.get(`/api/last_check_time`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.lastChecktime = res.data.last_check_time; + const res = await getLastChecktimeSvc({}); + this.lastChecktime = res.last_check_time; } catch (e: any) { this.toast.error(e.message); } @@ -121,12 +127,8 @@ export class SettingsModel { return false; } try { - const res = await axios.get(`/api/auto_update`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.isAutoCheck = res.data.auto_update; + const res = await getAutoUpdateSvc({}); + this.isAutoCheck = res.auto_update; return this.isAutoCheck; } catch (e: any) { this.toast.emitter.emit('message.error', e.message); @@ -140,12 +142,8 @@ export class SettingsModel { return false; } try { - const res = await axios.get(`/api/update_channel`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.isBetaCheck = res.data.update_channel !== 'stable'; + const res = await getUpdateChannelSvc({}); + this.isBetaCheck = res.update_channel !== 'stable'; return this.isBetaCheck; } catch (e: any) { this.toast.emitter.emit('message.error', e.message); @@ -157,17 +155,9 @@ export class SettingsModel { async setBetaCheck(isBetaCheck: boolean) { try { this.isBetaCheckLoading = true; - await axios.post( - `/api/update_channel`, - { - update_channel: isBetaCheck ? 'next' : 'stable', - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + await updateChannelSvc({ + update_channel: isBetaCheck ? 'next' : 'stable', + }); this.isBetaCheck = isBetaCheck; this.checkedStatus = null; } catch (e: any) { @@ -181,17 +171,9 @@ export class SettingsModel { async setAutoCheck(isAutoCheck: boolean) { try { this.isAutoCheckLoading = true; - await axios.post( - `/api/auto_update`, - { - auto_update: isAutoCheck, - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + await autoUpdateSvc({ + auto_update: isAutoCheck, + }); this.isAutoCheck = isAutoCheck; } catch (e: any) { this.toast.emitter.emit('message.error', e.message); @@ -204,12 +186,8 @@ export class SettingsModel { async checkNow() { try { this.isChecking = true; - const res = await axios.get(`/api/check_repo_status`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.checkRet = res.data; + const res = await checkRepoStatusSvc({}); + this.checkRet = res; if (this.checkRet.has_new_stable) { this.checkedStatus = 'newUpdate'; @@ -227,12 +205,8 @@ export class SettingsModel { @action.bound async getCurrentVersion() { try { - const res = await axios.get(`/api/check_repo_status`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - this.checkRet.current_version = res.data.current_version; + const res = await getCheckRepoStatusSvc({}); + this.checkRet.current_version = res.current_version; } catch (e: any) { this.toast.emitter.emit('message.error', e.message); } @@ -241,11 +215,7 @@ export class SettingsModel { @action.bound async restart() { this.isRestarting = true; - await axios.get(`/api/restart`, { - headers: { - 'Content-Type': 'application/json', - }, - }); + await restartSvc({}); const pollInterval = 3000; const poll = async () => { try { @@ -269,11 +239,7 @@ export class SettingsModel { async updateNow() { try { this.isUpdating = true; - await axios.get(`/api/update`, { - headers: { - 'Content-Type': 'application/json', - }, - }); + await updateSvc({}); this.isToRestart = true; } catch (e: any) { this.toast.emitter.emit('message.error', e.message); @@ -287,20 +253,15 @@ export class SettingsModel { if (settingsDisabled) return null; this.isLoadLoading = true; try { - const res = await axios.get(loadSettingEnvFormUrl, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const ret: any = res.data; + const res = await loadEnvSvc({}); this.envs = new Map(); - const envs = formatEnvs2Form(ret?.envs); + const envs = formatEnvs2Form(res?.envs); envs.forEach((item: any) => { this.envs.set(item.key, item.value); }); this.isLoadLoading = false; return { - ...res.data, + ...res, envs, }; } catch (e: any) { @@ -316,18 +277,10 @@ export class SettingsModel { value?.envs.forEach((item: any) => { this.envs.set(item.key, item.value); }); - await axios.post( - saveSettingEnvFormUrl, - { - ...value, - envs: formatEnvs2Api(value.envs), - }, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + await saveEnvSvc({ + ...value, + envs: formatEnvs2Api(value.envs), + }); } catch (e: any) { this.toast.emitter.emit('message.error', e.message); } diff --git a/web/apps/web/src/services/app/index.tsx b/web/apps/web/src/services/app/index.tsx index 06747c60..6a1793b5 100644 --- a/web/apps/web/src/services/app/index.tsx +++ b/web/apps/web/src/services/app/index.tsx @@ -26,6 +26,11 @@ import { GetAppVersionListResponse, } from './type'; import { APIFetch } from '../base'; +import { + loadSettingEnvFormUrl, + saveSettingEnvFormUrl, + SettingEnvFormValue, +} from '@/components/settings/settings-definitions'; // 保存 export const saveApp = (params: SaveAppRequest) => { @@ -127,3 +132,92 @@ export const importApp = (params: ExportBotResponse['data']) => { body: params, }); }; + +export const getLastChecktimeSvc: Fetcher< + { + last_check_time: string; + }, + {} +> = () => { + return APIFetch.get('/api/last_check_time'); +}; + +export const getAutoUpdateSvc: Fetcher< + { + auto_update: boolean; + }, + {} +> = () => { + return APIFetch.get('/api/auto_update'); +}; + +export const getUpdateChannelSvc: Fetcher< + { + update_channel: string; + }, + {} +> = () => { + return APIFetch.get('/api/update_channel'); +}; + +export const updateChannelSvc: Fetcher< + {}, + { + update_channel: string; + } +> = params => { + return APIFetch.post('/api/update_channel', { + body: params, + }); +}; + +export const autoUpdateSvc: Fetcher< + {}, + { + auto_update: boolean; + } +> = params => { + return APIFetch.post('/api/auto_update', { + body: params, + }); +}; + +export const checkRepoStatusSvc: Fetcher< + Partial<{ + has_new_stable: true; + target_release_date: string; + current_version: string; + latest_tag_name: string; + changelog: string; + }>, + {} +> = params => { + return APIFetch.get('/api/check_repo_status', { + body: params, + }); +}; + +export const getCheckRepoStatusSvc: Fetcher< + { + current_version: string; + }, + {} +> = () => { + return APIFetch.get('/api/update_channel'); +}; + +export const restartSvc: Fetcher<{}, {}> = () => { + return APIFetch.get('/api/restart'); +}; + +export const updateSvc: Fetcher<{}, {}> = () => { + return APIFetch.get(`/api/update`); +}; + +export const loadEnvSvc: Fetcher = () => { + return APIFetch.get(loadSettingEnvFormUrl); +}; + +export const saveEnvSvc: Fetcher = () => { + return APIFetch.get(saveSettingEnvFormUrl); +}; diff --git a/web/apps/web/src/services/app/message-type.ts b/web/apps/web/src/services/app/message-type.ts index d9190a98..ca173612 100644 --- a/web/apps/web/src/services/app/message-type.ts +++ b/web/apps/web/src/services/app/message-type.ts @@ -252,6 +252,7 @@ export interface ChatServerMessage { * 对齐主站 */ inputSetting?: MessageInputSetting; + message?: string; } /** diff --git a/web/apps/web/src/stores/global/global-provider.tsx b/web/apps/web/src/stores/global/global-provider.tsx index 8649ca67..93c603b6 100644 --- a/web/apps/web/src/stores/global/global-provider.tsx +++ b/web/apps/web/src/stores/global/global-provider.tsx @@ -2,7 +2,6 @@ import { type ReactNode, createContext, useRef, useContext } from 'react'; import { useStore } from 'zustand'; import { createStore } from 'zustand/vanilla'; -import { ManagerDialog } from '@/components/manager'; import { ChangelogDialog, SettingsDialog, @@ -66,7 +65,6 @@ export const GlobalStoreProvider = ({ children }: GlobalStoreProviderProps) => { return ( {children} - From d45c7a01dd2f3eaec22c1760b9e73a0f4a6a34bc Mon Sep 17 00:00:00 2001 From: kun Date: Thu, 9 Jan 2025 11:26:42 +0800 Subject: [PATCH 2/8] feat: hide in web --- web/apps/web/src/components/app/constants.tsx | 2 ++ .../src/components/app/task-list/index.tsx | 1 + .../common/list-footer-extra/index.tsx | 24 ++++++++++--------- .../src/components/layouts/side-content.tsx | 3 +++ .../src/components/layouts/side-footer.tsx | 4 ++++ web/apps/web/src/services/base.ts | 6 +++++ .../src/components/material-list/index.tsx | 1 + .../flow-engine/src/types/material-list.ts | 1 + 8 files changed, 31 insertions(+), 11 deletions(-) diff --git a/web/apps/web/src/components/app/constants.tsx b/web/apps/web/src/components/app/constants.tsx index 1be52954..770c83c5 100644 --- a/web/apps/web/src/components/app/constants.tsx +++ b/web/apps/web/src/components/app/constants.tsx @@ -137,6 +137,7 @@ export const materialList: MaterialListType = [ name: 'Workflow Runner', type: NodeTypeEnum.workflow, undraggable: true, + hidden: process.env.NEXT_PUBLIC_HIDE_IN_WEB === 'true', }, ], }, @@ -204,6 +205,7 @@ export const materialList: MaterialListType = [ type: NodeTypeEnum.widget, custom: true, undraggable: true, + hidden: process.env.NEXT_PUBLIC_HIDE_IN_WEB === 'true', }, ], }, diff --git a/web/apps/web/src/components/app/task-list/index.tsx b/web/apps/web/src/components/app/task-list/index.tsx index 3574f5c7..62accdd5 100644 --- a/web/apps/web/src/components/app/task-list/index.tsx +++ b/web/apps/web/src/components/app/task-list/index.tsx @@ -25,6 +25,7 @@ const TreeNode: React.FC<{ const hover = useHover(nodeRef); + if (data.hidden) return null; return (
{ feedbackModel.drawer.open(); }} /> - { - if (settingsDisabled) return; - settingsModel.modal.open(); - }} - /> + {process.env.NEXT_PUBLIC_HIDE_IN_WEB !== 'true' && ( + { + if (settingsDisabled) return; + settingsModel.modal.open(); + }} + /> + )}
); }; diff --git a/web/apps/web/src/components/layouts/side-content.tsx b/web/apps/web/src/components/layouts/side-content.tsx index 998ed7c4..a029ec43 100644 --- a/web/apps/web/src/components/layouts/side-content.tsx +++ b/web/apps/web/src/components/layouts/side-content.tsx @@ -13,6 +13,7 @@ interface ItemType { href: string; title: string; icon: React.ReactNode; + hidden?: boolean; } const ItemContent = ({ @@ -22,6 +23,7 @@ const ItemContent = ({ item: ItemType; isActive: (path: string) => boolean; }) => { + if (item.hidden) return null; return ( , }, ]; diff --git a/web/apps/web/src/components/layouts/side-footer.tsx b/web/apps/web/src/components/layouts/side-footer.tsx index befc9311..69e70f1a 100644 --- a/web/apps/web/src/components/layouts/side-footer.tsx +++ b/web/apps/web/src/components/layouts/side-footer.tsx @@ -24,6 +24,7 @@ interface ItemType { title: string; icon: React.ReactNode; passHref?: boolean; + hidden?: boolean; onClick?: () => void; disabled?: boolean; } @@ -44,7 +45,9 @@ const ItemFooter = ({ passHref, onClick, disabled, + hidden, }: ItemType) => { + if (hidden) return null; return ( { if (settingsDisabled) return; diff --git a/web/apps/web/src/services/base.ts b/web/apps/web/src/services/base.ts index aea1e0c8..e880e298 100644 --- a/web/apps/web/src/services/base.ts +++ b/web/apps/web/src/services/base.ts @@ -112,6 +112,12 @@ export const customFetch = async ( result = response; } + if (response.status === 401 && process.env.NEXT_PUBLIC_LOGIN_URL) { + const errorMessage = `Authentication failed! status: ${response.status}`; + reject(new Error(errorMessage)); + window.location.href = `${process.env.NEXT_PUBLIC_LOGIN_URL}?login=true&redirect=${window.location.href}`; + } + if (response.status > 400) { const errorMessage = `HTTP error! status: ${response.status}`; reject(new Error(errorMessage)); diff --git a/web/packages/flow-engine/src/components/material-list/index.tsx b/web/packages/flow-engine/src/components/material-list/index.tsx index 81c413d6..1db749ee 100644 --- a/web/packages/flow-engine/src/components/material-list/index.tsx +++ b/web/packages/flow-engine/src/components/material-list/index.tsx @@ -66,6 +66,7 @@ const DraggableTreeNode: React.FC<{ data: WidgetItem }> = ({ data }) => { } }; + if (data.hidden) return null; return (
JSX.Element; widget_name?: string; }; From 74697ed1d39ab0ac648ada128921075b2d50eb2b Mon Sep 17 00:00:00 2001 From: kun Date: Fri, 10 Jan 2025 14:40:29 +0800 Subject: [PATCH 3/8] feat: api --- web/apps/web/next.config.js | 3 +- web/apps/web/src/app/app/page.tsx | 16 ++-- web/apps/web/src/app/container.ts | 3 +- .../components/chat/app-builder-chat.model.ts | 10 +- .../common/performance-monitor/index.tsx | 91 ------------------- web/apps/web/src/services/app/index.tsx | 3 +- web/apps/web/src/services/base.ts | 9 +- web/apps/web/src/services/common/index.ts | 25 +++-- 8 files changed, 41 insertions(+), 119 deletions(-) delete mode 100644 web/apps/web/src/components/common/performance-monitor/index.tsx diff --git a/web/apps/web/next.config.js b/web/apps/web/next.config.js index 34dfd9d6..764fa90d 100644 --- a/web/apps/web/next.config.js +++ b/web/apps/web/next.config.js @@ -13,7 +13,7 @@ module.exports = { '@shellagent/form-engine', ], images: { - unoptimized: false, + unoptimized: true, }, webpack: ( config, @@ -50,6 +50,7 @@ module.exports = { return config; }, rewrites: async () => { + // console.log("process.env.NEXT_PUBLIC_SHELL_API_URL", process.env.NEXT_PUBLIC_SHELL_API_URL) return [ { source: '/api/:path*', diff --git a/web/apps/web/src/app/app/page.tsx b/web/apps/web/src/app/app/page.tsx index 897a4a25..4603382f 100644 --- a/web/apps/web/src/app/app/page.tsx +++ b/web/apps/web/src/app/app/page.tsx @@ -20,13 +20,15 @@ export default function AppPage() { const settingsModel = useInjection(SettingsModel); useEffect(() => { - (async function () { - await settingsModel.getIsBetaCheck(); - const isAutoCheck = await settingsModel.getAutoCheck(); - if (isAutoCheck) { - settingsModel.autoCheck(); - } - })(); + if (process.env.NEXT_PUBLIC_HIDE_IN_WEB !== 'true') { + (async function () { + await settingsModel.getIsBetaCheck(); + const isAutoCheck = await settingsModel.getAutoCheck(); + if (isAutoCheck) { + settingsModel.autoCheck(); + } + })(); + } }, []); const { data, isLoading, mutate } = useSWR('/api/list?type=app', () => diff --git a/web/apps/web/src/app/container.ts b/web/apps/web/src/app/container.ts index f2b97eef..14a89812 100644 --- a/web/apps/web/src/app/container.ts +++ b/web/apps/web/src/app/container.ts @@ -7,10 +7,11 @@ import { ImageCanvasModel } from 'image-canvas/model'; import { Container, interfaces } from 'inversify'; import * as Mobx from 'mobx'; import { toast } from 'react-toastify'; + import { ComfyUIModel } from '@/components/app/plugins/comfyui/comfyui.model'; import { AssistantModel } from '@/components/assistant/model'; -import { FeedbackModel } from '@/components/feedback/model'; import { AppBuilderChatModel } from '@/components/chat/app-builder-chat.model'; +import { FeedbackModel } from '@/components/feedback/model'; import { OpenImageCanvasModel } from '@/components/image-canvas/open-image-canvas.model'; import { SettingsModel } from '@/components/settings/settings.model'; import { AppBuilderModel } from '@/stores/app/models/app-builder.model'; diff --git a/web/apps/web/src/components/chat/app-builder-chat.model.ts b/web/apps/web/src/components/chat/app-builder-chat.model.ts index 8c5b9c20..a1939f60 100644 --- a/web/apps/web/src/components/chat/app-builder-chat.model.ts +++ b/web/apps/web/src/components/chat/app-builder-chat.model.ts @@ -21,6 +21,7 @@ import type { ServerMessage } from '../../services/app/message-type'; import { EventStatusEnum, RunAppRequest } from '../../services/app/type'; import { ToastModel } from '../../utils/toast.model'; import { initBot } from '@/services/app'; +import { baseHeaders } from '@/services/base'; @injectable() export class AppBuilderChatModel { @@ -137,14 +138,14 @@ export class AppBuilderChatModel { this.isInitBotLoading = true; }); try { - const reuslt = await initBot({ + const result = await initBot({ automata, }); - if (reuslt.data.session_id == null && reuslt.data.message) { - this.emitter.emitter.emit('message.error', reuslt.data.message); + if (result.session_id == null && result.message) { + this.emitter.emitter.emit('message.error', result.message); return; } - this.session_id = reuslt.data.session_id; + this.session_id = result.session_id; this.openRunDrawer(); await this.chatNew.isReadyPromise; this.chatNew.clearMemory(); @@ -187,6 +188,7 @@ export class AppBuilderChatModel { headers: { method: 'POST', 'Content-Type': 'application/json', + ...baseHeaders, }, body: JSON.stringify({ session_id: this.session_id, diff --git a/web/apps/web/src/components/common/performance-monitor/index.tsx b/web/apps/web/src/components/common/performance-monitor/index.tsx deleted file mode 100644 index 02fc58ec..00000000 --- a/web/apps/web/src/components/common/performance-monitor/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useRequest } from 'ahooks'; -import React from 'react'; - -interface MonitorData { - cpu_memory_used: number; - cpu_memory_total: number; - gpu_memory_used: number; - gpu_memory_total: number; -} - -async function fetchMonitorData(): Promise { - const response = await fetch('/api/tools/monitor_memory', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }); - if (!response.ok) throw new Error('Failed to fetch monitor data'); - return response.json(); -} - -const MemoryBar = ({ percentage }: { percentage: string }) => { - const getColor = (percentage: number) => { - if (percentage < 50) return 'bg-green-500 text-green-500'; - if (percentage < 80) return 'bg-orange-500 text-orange-500'; - return 'bg-red-500 text-red-500'; - }; - - const color = getColor(parseFloat(percentage)).split(' ')[0]; - return ( -
-
-
- ); -}; - -export default function PerformanceMonitor() { - const isDev = process.env.NEXT_PUBLIC_DEVELOPMENT === 'yes'; - const { data } = useRequest(fetchMonitorData, { - pollingInterval: isDev ? 120 * 1000 : 3000, - pollingWhenHidden: false, - }); - - if (!data) return null; - - const formatUsage = (used: number, total: number) => { - const percentage = ((used / total) * 100).toFixed(1); - const usedGB = used.toFixed(1); - const totalGB = total.toFixed(1); - return { percentage, usedGB, totalGB }; - }; - - const getColor = (percentage: number) => { - if (percentage < 50) return 'bg-green-500 text-green-500'; - if (percentage < 80) return 'bg-orange-500 text-orange-500'; - return 'bg-red-500 text-red-500'; - }; - - const cpuUsage = formatUsage(data.cpu_memory_used, data.cpu_memory_total); - const gpuUsage = formatUsage(data.gpu_memory_used, data.gpu_memory_total); - - const cpuColor = getColor(parseFloat(cpuUsage.percentage)).split(' ')[1]; - const gpuColor = getColor(parseFloat(gpuUsage.percentage)).split(' ')[1]; - - return ( -
-
-
- CPU - {cpuUsage.percentage}% -
- -
- {cpuUsage.usedGB} / {cpuUsage.totalGB}GB -
-
-
-
- GPU - {gpuUsage.percentage}% -
- -
- {gpuUsage.usedGB} / {gpuUsage.totalGB}GB -
-
-
- ); -} diff --git a/web/apps/web/src/services/app/index.tsx b/web/apps/web/src/services/app/index.tsx index 6a1793b5..dd627db0 100644 --- a/web/apps/web/src/services/app/index.tsx +++ b/web/apps/web/src/services/app/index.tsx @@ -25,7 +25,7 @@ import { GetAppVersionListRequest, GetAppVersionListResponse, } from './type'; -import { APIFetch } from '../base'; +import { APIFetch, baseHeaders } from '../base'; import { loadSettingEnvFormUrl, saveSettingEnvFormUrl, @@ -86,6 +86,7 @@ export const runApp = ( headers: { method: 'POST', 'Content-Type': 'application/json', + ...baseHeaders, }, body: JSON.stringify(params), signal: abortController.signal, diff --git a/web/apps/web/src/services/base.ts b/web/apps/web/src/services/base.ts index e880e298..1e6100fd 100644 --- a/web/apps/web/src/services/base.ts +++ b/web/apps/web/src/services/base.ts @@ -32,6 +32,10 @@ const baseOptions = { redirect: 'follow', }; +export const baseHeaders = { + 'myshell-service-name': 'organics-api', +}; + // 通用fetch,后续再次基础上拓展 export const customFetch = async ( url: string, @@ -91,7 +95,10 @@ export const customFetch = async ( globalThis .fetch(url, { ...options, - headers, + headers: { + ...baseHeaders, + ...headers, + }, } as RequestInit) .then(response => { const contentType = response.headers.get('content-type'); diff --git a/web/apps/web/src/services/common/index.ts b/web/apps/web/src/services/common/index.ts index 7c0e397e..cfc96f00 100644 --- a/web/apps/web/src/services/common/index.ts +++ b/web/apps/web/src/services/common/index.ts @@ -1,4 +1,5 @@ import { toast } from 'react-toastify'; +import { APIFetch } from '../base'; export interface UploadResponse { url: string; @@ -10,21 +11,19 @@ export async function upload( ): Promise { const formData = new FormData(); formData.append('file', file, name || file.name); - const response: { file_path: string } = await fetch('/api/upload', { - method: 'post', + const response = await APIFetch.post<{ file_path: string }>('/api/upload', { body: formData, - }) - .then(res => res.json()) - .catch(() => { - toast.error('upload error', { - position: 'top-center', - autoClose: 2000, - hideProgressBar: true, - pauseOnHover: true, - closeButton: false, - }); + }).catch(() => { + toast.error('upload error', { + position: 'top-center', + autoClose: 2000, + hideProgressBar: true, + pauseOnHover: true, + closeButton: false, }); + }); + console.log('response: ', response); return { - url: response?.file_path, + url: response?.file_path || '', }; } From 91849b1abe1e6b5fc025f280eb28b9921718e517 Mon Sep 17 00:00:00 2001 From: kun Date: Fri, 10 Jan 2025 20:36:13 +0800 Subject: [PATCH 4/8] feat: pull beta --- web/apps/web/src/services/app/type.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/apps/web/src/services/app/type.ts b/web/apps/web/src/services/app/type.ts index 7c352866..fac82b3a 100644 --- a/web/apps/web/src/services/app/type.ts +++ b/web/apps/web/src/services/app/type.ts @@ -81,9 +81,7 @@ export interface InitBotRequest { automata: Automata; } -export interface InitBotResponse { - data: ServerMessage; -} +export interface InitBotResponse extends ServerMessage {} /** * 聊天信息, 一般是传给后端处理 From 03bc0fdb3c3f7be6dc58bdc2cbf9eb422a2281ed Mon Sep 17 00:00:00 2001 From: kun Date: Fri, 10 Jan 2025 21:42:11 +0800 Subject: [PATCH 5/8] feat: content type --- web/apps/web/src/services/common/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/apps/web/src/services/common/index.ts b/web/apps/web/src/services/common/index.ts index cfc96f00..23164bcb 100644 --- a/web/apps/web/src/services/common/index.ts +++ b/web/apps/web/src/services/common/index.ts @@ -13,6 +13,9 @@ export async function upload( formData.append('file', file, name || file.name); const response = await APIFetch.post<{ file_path: string }>('/api/upload', { body: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, }).catch(() => { toast.error('upload error', { position: 'top-center', From a1a8e86f2cddb34052f75d312c6513fab22233c1 Mon Sep 17 00:00:00 2001 From: kun Date: Mon, 13 Jan 2025 15:29:44 +0800 Subject: [PATCH 6/8] feat: api files --- .../app/header/extra-action/import-modal.tsx | 3 ++- .../components/common/file-render/index.tsx | 26 +++++++------------ .../common/text-file-editor/index.tsx | 4 +-- .../common/uploader/display/index.tsx | 3 ++- .../node-form/widgets/render/display.tsx | 8 +++--- web/apps/web/src/services/base.ts | 10 ++++--- web/apps/web/src/services/common/index.ts | 3 --- .../web/src/stores/app/utils/validator.ts | 6 ++--- web/apps/web/src/utils/common-helper.ts | 5 ++++ 9 files changed, 31 insertions(+), 37 deletions(-) diff --git a/web/apps/web/src/components/app/header/extra-action/import-modal.tsx b/web/apps/web/src/components/app/header/extra-action/import-modal.tsx index d5272f67..da6bfebc 100644 --- a/web/apps/web/src/components/app/header/extra-action/import-modal.tsx +++ b/web/apps/web/src/components/app/header/extra-action/import-modal.tsx @@ -15,6 +15,7 @@ import { toast } from 'react-toastify'; import FileUploader from '@/components/common/uploader'; import { ExportBotResponse } from '@/services/app/type'; import { APIFetch } from '@/services/base'; +import { getFileUrl } from '@/utils/common-helper'; const ImportModal: React.FC<{ open: boolean; @@ -31,7 +32,7 @@ const ImportModal: React.FC<{ useEffect(() => { if (fileUrl) { setLoading(true); - APIFetch.get(`/api/files/${fileUrl}`) + APIFetch.get(getFileUrl(fileUrl)) .then(({ data, success }) => { if (!success) { throw new Error(''); diff --git a/web/apps/web/src/components/common/file-render/index.tsx b/web/apps/web/src/components/common/file-render/index.tsx index 4be071ca..c600b166 100644 --- a/web/apps/web/src/components/common/file-render/index.tsx +++ b/web/apps/web/src/components/common/file-render/index.tsx @@ -5,15 +5,19 @@ import { ImageRex, VideoRex, AudioRex } from '@/utils/file-types'; import Audio from '../uploader/display/audio'; +const getFileUrl = (url: string) => { + if (url?.startsWith('http')) return url; + if (url?.startsWith('data/upload')) return `/api/files/${url}`; + return `/api/files/input/${url}`; +}; + const FileRender = ({ url }: { url: string }) => { if (ImageRex.test(url)) { return ( { if (VideoRex.test(url)) { return ( { if (AudioRex.test(url)) { return (
-
); } diff --git a/web/apps/web/src/components/common/text-file-editor/index.tsx b/web/apps/web/src/components/common/text-file-editor/index.tsx index 8e67c50b..10d9b9c8 100644 --- a/web/apps/web/src/components/common/text-file-editor/index.tsx +++ b/web/apps/web/src/components/common/text-file-editor/index.tsx @@ -5,7 +5,7 @@ import TextareaAutosize from 'react-textarea-autosize'; import { toast } from 'react-toastify'; import { upload } from '@/services/common'; -import { generateUUID } from '@/utils/common-helper'; +import { generateUUID, getFileUrl } from '@/utils/common-helper'; import { FileStatus } from '../uploader'; @@ -90,7 +90,7 @@ const TextFileEditor = (props: { onClick={() => handleDeleteImage(idx)} className="w-4 h-4 z-10 absolute top-[-4px] right-[-4px] hidden group-hover/menu:flex" /> - +
))}
diff --git a/web/apps/web/src/components/common/uploader/display/index.tsx b/web/apps/web/src/components/common/uploader/display/index.tsx index 48471eb9..325998a6 100644 --- a/web/apps/web/src/components/common/uploader/display/index.tsx +++ b/web/apps/web/src/components/common/uploader/display/index.tsx @@ -8,6 +8,7 @@ import Image from '@/components/common/uploader/display/image'; import Other from '@/components/common/uploader/display/other'; import Video from '@/components/common/uploader/display/video'; import { ENABLE_PREVIEW_FILE, getLocalTypeBySuffix } from '@/utils/file-types'; +import { getFileUrl } from '@/utils/common-helper'; type DisplayType = 'audio' | 'video' | 'image' | 'other'; @@ -46,7 +47,7 @@ export default function Display({ if (typeof file === 'string') { const [filename, suffix] = last(file.split('/'))?.split('.') || []; const filetype = getLocalTypeBySuffix(suffix); - const fileurl = file.startsWith('http') ? file : `/api/files/${file}`; + const fileurl = getFileUrl(file); return [fileurl, filename, filetype]; } const { name, size, type } = file; diff --git a/web/apps/web/src/components/workflow/node-form/widgets/render/display.tsx b/web/apps/web/src/components/workflow/node-form/widgets/render/display.tsx index 7c3ba8f1..f38808b8 100644 --- a/web/apps/web/src/components/workflow/node-form/widgets/render/display.tsx +++ b/web/apps/web/src/components/workflow/node-form/widgets/render/display.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import ReactPlayer from 'react-player'; import { cn } from '@/utils/cn'; +import { getFileUrl } from '@/utils/common-helper'; const ImageRex = /.(jpg|jpeg|png|apng|gif|bmp|tiff|webp)$/; const VideoRex = /.(mp4|avi|mov|wmv|flv)$/; @@ -56,7 +57,7 @@ interface ImageLayoutProps { const ImageLayout = ({ field, images }: ImageLayoutProps) => { const generateImageConfig = (src: string) => ({ - src: `/api/files/${src}`, + src: getFileUrl(src), wrapperClassName: 'rounded-lg overflow-hidden', }); return ( @@ -96,10 +97,7 @@ const VideoLayout = ({ field, videos }: VideoLayoutProps) => { } content={ - `/api/files/${url}`)} - width="100%" - /> + getFileUrl(url))} width="100%" /> } /> ); diff --git a/web/apps/web/src/services/base.ts b/web/apps/web/src/services/base.ts index 1e6100fd..812fdd0f 100644 --- a/web/apps/web/src/services/base.ts +++ b/web/apps/web/src/services/base.ts @@ -26,9 +26,9 @@ const baseOptions = { method: 'GET', mode: 'cors', credentials: 'include', // always send cookies、HTTP Basic authentication. - headers: { - 'Content-Type': ContentType.json, - }, + // headers: { + // 'Content-Type': ContentType.json, + // }, redirect: 'follow', }; @@ -122,7 +122,9 @@ export const customFetch = async ( if (response.status === 401 && process.env.NEXT_PUBLIC_LOGIN_URL) { const errorMessage = `Authentication failed! status: ${response.status}`; reject(new Error(errorMessage)); - window.location.href = `${process.env.NEXT_PUBLIC_LOGIN_URL}?login=true&redirect=${window.location.href}`; + window.location.href = `${ + process.env.NEXT_PUBLIC_LOGIN_URL + }?login=true&redirect=${decodeURIComponent(window.location.href)}`; } if (response.status > 400) { diff --git a/web/apps/web/src/services/common/index.ts b/web/apps/web/src/services/common/index.ts index 23164bcb..cfc96f00 100644 --- a/web/apps/web/src/services/common/index.ts +++ b/web/apps/web/src/services/common/index.ts @@ -13,9 +13,6 @@ export async function upload( formData.append('file', file, name || file.name); const response = await APIFetch.post<{ file_path: string }>('/api/upload', { body: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, }).catch(() => { toast.error('upload error', { position: 'top-center', diff --git a/web/apps/web/src/stores/app/utils/validator.ts b/web/apps/web/src/stores/app/utils/validator.ts index 69dd281d..d9bb0974 100644 --- a/web/apps/web/src/stores/app/utils/validator.ts +++ b/web/apps/web/src/stores/app/utils/validator.ts @@ -1,3 +1,4 @@ +import { getFileUrl } from '@/utils/common-helper'; import { IFlow } from '@shellagent/flow-engine'; import { Automata } from '@shellagent/pro-config'; import { @@ -61,10 +62,7 @@ export async function validateImage( if (!url || url === '/') { return Promise.resolve(true); } - const apiUrl = - url?.startsWith('/api/files') || url?.startsWith('http') - ? url - : `/api/files/${url}`; + const apiUrl = getFileUrl(url); try { const response = await fetch(apiUrl, { method: 'GET' }); const contentLength = response.headers.get('content-length'); diff --git a/web/apps/web/src/utils/common-helper.ts b/web/apps/web/src/utils/common-helper.ts index 0e1da293..6798ba0a 100644 --- a/web/apps/web/src/utils/common-helper.ts +++ b/web/apps/web/src/utils/common-helper.ts @@ -155,3 +155,8 @@ export const filterVariable = (data: Record) => { {} as any, ); }; + +export const getFileUrl = (url: string, prefix?: string) => { + if (url?.startsWith('http') || url?.startsWith('/api/files')) return url; + return `/api/files/${url}`; +}; From c1a97393342e5358e952cb4b724dc6a8a2ed5cce Mon Sep 17 00:00:00 2001 From: Wenliang Zhao Date: Wed, 15 Jan 2025 17:02:13 +0800 Subject: [PATCH 7/8] Dev comfyui v2 (#251) Co-authored-by: kun Co-authored-by: joe Co-authored-by: yuxumin <1090414006@qq.com> Co-authored-by: Chengwei Ouyang <118652142+Cherwayway@users.noreply.github.com> Co-authored-by: shanexi Co-authored-by: tiancheng-myshell <158017297+tiancheng-myshell@users.noreply.github.com> Co-authored-by: Chengwei Ouyang Co-authored-by: myshell-leon --- proconfig/core/block.py | 4 +- proconfig/core/common.py | 2 +- proconfig/core/render.py | 6 +- proconfig/core/state.py | 3 +- proconfig/core/variables.py | 4 +- proconfig/runners/runner.py | 55 +++++-- proconfig/utils/x_bot.py | 141 ++++++++++++++++++ .../widgets/imagen_widgets/comfyui_widget.py | 11 +- .../widgets/language_models/claude_widgets.py | 1 + proconfig/widgets/myshell_widgets/__init__.py | 3 +- .../myshell_widgets/myshell_widget_caller.py | 90 +++++++++++ .../widgets/myshell_widgets/tools/html2img.py | 3 +- .../myshell_widgets/tools/image_canvas.py | 2 +- .../myshell_widgets/tools/image_text_fuser.py | 2 +- .../myshell_widgets/tools/twitter_search.py | 3 +- proconfig/widgets/tools/code_runner.py | 2 +- servers/automata.py | 59 +++++--- servers/main.py | 1 + servers/widgets.py | 116 ++++++++++++++ servers/workflow.py | 32 ++-- 20 files changed, 475 insertions(+), 65 deletions(-) create mode 100644 proconfig/utils/x_bot.py create mode 100644 proconfig/widgets/myshell_widgets/myshell_widget_caller.py create mode 100644 servers/widgets.py diff --git a/proconfig/core/block.py b/proconfig/core/block.py index 95918c2c..e78cf475 100755 --- a/proconfig/core/block.py +++ b/proconfig/core/block.py @@ -11,7 +11,7 @@ class Block(BaseModel): type: BlockType = Field(..., description="type of the block") name: str = Field("", description="name of the block") - display_name: str = Field(None, description="the display name of the block") + display_name: str = Field("", description="the display name of the block") properties: Dict[str, Any] = Field({}, description="to specify other properties") inputs: Dict[CustomKey, Union[Input, Value]] = {} outputs: Dict[Union[CustomKey, ContextCustomKey], Union[Variable, Value]] = {} @@ -64,7 +64,7 @@ def sanity_check(self): class BaseProp(BaseModel): - cache: bool = None + cache: bool = False class TaskBase(Block): type: Literal["task"] diff --git a/proconfig/core/common.py b/proconfig/core/common.py index fb83bb72..2753637b 100755 --- a/proconfig/core/common.py +++ b/proconfig/core/common.py @@ -17,7 +17,7 @@ 'transitions', 'states', 'context', - 'payload', + # 'payload', 'condition', 'blocks', 'buttons', diff --git a/proconfig/core/render.py b/proconfig/core/render.py index b9fa02ce..a639e435 100755 --- a/proconfig/core/render.py +++ b/proconfig/core/render.py @@ -1,8 +1,8 @@ from typing import Union, Dict, List, Optional from pydantic import BaseModel, Field -from .common import CustomKey, CustomEventName -from .variables import URLString, Expression, Value +from proconfig.core.common import CustomKey, CustomEventName +from proconfig.core.variables import URLString, Expression, Value class EventPayload(BaseModel): @@ -14,7 +14,7 @@ class Button(BaseModel): content: str = "" description: Optional[str] = Field(default=None, description="Tooltip when hovering button") on_click: Union[CustomEventName, EventPayload] = Field(..., description="event name triggered") - style: Dict[str, str] = None # TODO + style: Dict[str, str] | None = None # TODO class RenderConfig(BaseModel): diff --git a/proconfig/core/state.py b/proconfig/core/state.py index b41d1333..d1a0de51 100755 --- a/proconfig/core/state.py +++ b/proconfig/core/state.py @@ -12,7 +12,7 @@ class StateProp(BaseProp): is_final: bool = False class State(Block): - type: Literal["state"] = "state" + type: Literal["state", "dispatcher"] = "state" properties: StateProp = StateProp() blocks: BlockChildren[Union[Task, Workflow]] = Field({}) render: RenderConfig = RenderConfig() @@ -38,6 +38,7 @@ def sanity_check(self): def check_input_IM(self): IM_count = 0 for k, v in self.inputs.items(): + print(k, v) if v.type in ["text", "string"] and v.source == "IM": IM_count += 1 if IM_count > 1: diff --git a/proconfig/core/variables.py b/proconfig/core/variables.py index d20c1902..06901b4f 100755 --- a/proconfig/core/variables.py +++ b/proconfig/core/variables.py @@ -13,7 +13,7 @@ class VariableBase(BaseModel): type: str - name: str = None + name: str = "" value: Any = None @@ -73,7 +73,7 @@ class InputVariableBase(VariableBase, Generic[T]): default_value: Union[T, Expression] | None = None value: Union[T, Expression] | None = None user_input: bool = False - source: Literal["IM", "form"] = None + source: Literal["IM", "form"] = "form" class InputTextVar(TextVar, InputVariableBase[str], ChoicesBase[str]): type: Literal["text", "string"] diff --git a/proconfig/runners/runner.py b/proconfig/runners/runner.py index 818a8747..92f21059 100755 --- a/proconfig/runners/runner.py +++ b/proconfig/runners/runner.py @@ -220,13 +220,18 @@ def run_block_task(self, task, environ, local_vars): local_vars[task.name] = outputs def process_comfy_extra_inputs(self, task): - comfy_extra_inputs = { "api": task.api, "comfy_workflow_id": task.comfy_workflow_id, "location": task.location, } task.inputs["comfy_extra_inputs"] = comfy_extra_inputs + + def process_myshell_extra_inputs(self, task): + task.inputs = { + "widget_id": task.widget_name.split("/")[1], + "inputs": task.inputs + } def run_task(self, container, task, environ, local_vars): @@ -234,6 +239,8 @@ def run_task(self, container, task, environ, local_vars): if task.widget_class_name == "ComfyUIWidget": # hasattr(task, "comfy_workflow_id") assert hasattr(task, "comfy_workflow_id"), "no comfy_workflow_id is founded" self.process_comfy_extra_inputs(task) + elif task.widget_class_name == "MyShellAnyWidgetCallerWidget": + self.process_myshell_extra_inputs(task) if task.mode in ["widget", "comfy_workflow"]: return self.run_widget_task(container, task, environ, local_vars) @@ -564,7 +571,7 @@ def run_automata(self, automata: Automata, sess_state: SessionState, payload: di local_transitions = automata.blocks[current_state_name].transitions or {} global_transitions = automata.transitions or {} - events = [] + events = {} for button_id, button in enumerate(render.get("buttons", [])): event_name = button["on_click"] if isinstance(event_name, dict): @@ -572,19 +579,32 @@ def run_automata(self, automata: Automata, sess_state: SessionState, payload: di event_name = event_name["event"] else: event_payload = {} - events.append({ - "event_key": f"BUTTON_{button_id}", + event_key = f"BUTTON_{button_id}" + events[event_key] = { "event_name": event_name, - "payload": event_payload - }) + "payload": event_payload, + "is_button": True + } - events.append({ - "event_key": "CHAT", - "event_name": "CHAT", - "payload": {} - }) + events["CHAT"] = { + "event_name": "CHAT", + "payload": {} + } + + skip_evaluate_events = ["X.MENTIONED", "X.PINNED.REPLIED"] + + # final event mapping, not target_inputs + for key, transition in local_transitions.items(): + # evaluate the transitions + if key not in events: + events[key] = { + "event_name": key, + "payload": {} + } + if key in skip_evaluate_events: + events[key]["skip_evaluate_target_inputs"] = True - for event_item in events: + for event_key, event_item in events.items(): event_name = event_item["event_name"] event_payload = event_item["payload"] for payload_key, payload_value in event_payload.items(): @@ -609,7 +629,10 @@ def run_automata(self, automata: Automata, sess_state: SessionState, payload: di # get the target_inputs from transition target_inputs_transition = {} for k, v in transition_case.target_inputs.items(): - target_inputs_transition[k] = calc_expression(v, {'payload': event_payload, **local_vars}) # the target_inputs defined by the transition + if event_item.get("skip_evaluate_target_inputs", False): + target_inputs_transition[k] = v + else: + target_inputs_transition[k] = calc_expression(v, {'payload': event_payload, **local_vars}) # the target_inputs defined by the transition # get the target inputs target_inputs = automata.blocks[target_state].inputs @@ -625,14 +648,16 @@ def run_automata(self, automata: Automata, sess_state: SessionState, payload: di setattr(input_var, k, tree_map(lambda x: calc_expression(x, visible_variables), getattr(input_var, k))) # target_inputs = tree_map(lambda x: calc_expression(x, local_vars), target_inputs) - event_mapping[event_item["event_key"]] = EventItem(**{ + event_mapping[event_key] = EventItem(**{ "target_state": target_state, "target_inputs": target_inputs, "target_inputs_transition": target_inputs_transition, # the target_inputs from the transition }) + + message_count = sess_state.message_count - event_mapping = {f'MESSAGE_{message_count}_{k}' if k != "CHAT" else k: v for k, v in event_mapping.items()} + event_mapping = {f'MESSAGE_{message_count}_{k}' if events[k].get("is_button", False) else k: v for k, v in event_mapping.items()} sess_state.event_mapping.update(event_mapping) return sess_state, render \ No newline at end of file diff --git a/proconfig/utils/x_bot.py b/proconfig/utils/x_bot.py new file mode 100644 index 00000000..ac2a4997 --- /dev/null +++ b/proconfig/utils/x_bot.py @@ -0,0 +1,141 @@ +import json +from pydantic import BaseModel +from typing import List, Literal +from proconfig.core import Automata, State +from proconfig.core.automata import StateWithTransitions +from proconfig.core.variables import InputTextVar +from proconfig.core.common import TransitionCase +from proconfig.core.render import Button +from proconfig.utils.misc import tree_map +from datetime import date + + +class TwitterUser(BaseModel): + creation_date: str = str(date.today()) + user_id: str = "user_id_123" + username: str = "username_default" + name: str = "Default Name" + is_private: bool = False + is_verified: bool = False + is_blue_verified: bool = False + bot: bool = False + verified_type: Literal['blue', 'business', 'government'] = 'blue' + + +class TweetDetail(BaseModel): + tweet_id: str = "tweet_id_123" + creation_date: str = str(date.today()) + text: str = "This is an example text" + user: TwitterUser = TwitterUser() + media_url: List[str] = [] + + +x_special_events = { + "X.MENTIONED": { + "button_name": "Reply to mentioned", + }, + "X.PINNED.REPLIED": { + "button_name": "Reply to pinned tweet" + }, +} + + # content: str = "" + # description: Optional[str] = Field(default=None, description="Tooltip when hovering button") + # on_click: Union[CustomEventName, EventPayload] = Field(..., description="event name triggered") + # style: Dict[str, str] = None # TODO + +def replace_payload_to_payload1(x): + if type(x) == str: + x = x.replace("payload.", "payload1.") + return x + +def convert_x_bot_to_automata(automata_x): + # build default_payload + default_payload = TweetDetail().model_dump_json(indent=2) + # user = default_payload["user"] + # default_payload["user"] = f"{{{{{user}}}}}" + + # automata_x = Automata.model_validate(automata_x) + + + new_states = {} + automata_x["blocks"] = automata_x.get("blocks", {}) + for state_name, state in automata_x["blocks"].items(): + timers = {} + state["properties"] = state.get("properties", {}) + if "timers" in state["properties"]: + for timer_key, timer_info in state["properties"]["timers"].items(): + timers[timer_info["event"]] = timer_key + + state["render"] = state.get("render", {}) + buttons = state["render"].get("buttons", []) + # buttons = state.render.buttons + # print("buttons:", buttons) + # first, handle the special events + count = 0 + state["transitions"] = state.get("transitions") or {} + for event_name, transition_cases in state["transitions"].items(): + if event_name in x_special_events: + new_button = { + "content": x_special_events[event_name]["button_name"], + "on_click": { + "event": event_name, + "payload": {} + } + } + # add a pseudo state + new_state = { + "display_name": "", + "inputs": { + "payload_str": { + "type": "text", + "name": "payload_str", + "default_value": default_payload, + "user_input": True, + "source": "form" + } + }, + "outputs": { + "payload1": "{{json.loads(payload_str)}}" + }, + "transitions": { + "ALWAYS": tree_map(replace_payload_to_payload1, transition_cases) + } + } + new_state_name = f"{state_name}_event_{count}" + # state.transitions[event_name] = TransitionCase(target=new_state_name) + # new_states[new_state_name] = StateWithTransitions.model_validate(new_state) + + state["transitions"][event_name] = { + "target": new_state_name + } + new_states[new_state_name] = new_state + elif event_name in timers: + new_button = { + "content": f"Timer [{timers[event_name]}] triggered", + "on_click": { + "event": event_name, + "payload": {} + } + } + else: + continue + count += 1 + buttons.append(new_button) + # buttons.append(Button.model_validate(new_button)) + state["render"]["buttons"] = buttons + automata_x["blocks"].update(new_states) + return automata_x + return automata_x.model_dump() + + +if __name__ == "__main__": + automata_x_hello = json.load(open("temp/automata_x.json")) + automata_hello = convert_x_bot_to_automata(automata_x_hello) + # import pdb; pdb.set_trace() + # InputTextVar.model_validate(automata_hello["blocks"]["idle_event_0"]["inputs"]["payload_str"]) + # automata_hello_load = Automata.model_validate(automata_hello) + json.dump(automata_hello, open("temp/automata_x_converted.json", "w"), indent=2) + + + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/proconfig/widgets/imagen_widgets/comfyui_widget.py b/proconfig/widgets/imagen_widgets/comfyui_widget.py index a8d9a8e6..ffcde839 100644 --- a/proconfig/widgets/imagen_widgets/comfyui_widget.py +++ b/proconfig/widgets/imagen_widgets/comfyui_widget.py @@ -383,7 +383,16 @@ def comfyui_run_myshell(workflow_id, inputs, extra_headers): response = requests.post(url, headers=headers, json=data) # Parse the JSON response if response.status_code == 200: - return json.loads(response.json()['result']) + response_json = response.json() + if response_json["success "]: + return json.loads(response.json()['result']) + else: + error = { + 'error_code': 'SHELL-1109', + 'error_head': 'HTTP Request Error', + 'msg': response_json["error_message"], + } + raise ShellException(**error) else: try: response_content = response.json() diff --git a/proconfig/widgets/language_models/claude_widgets.py b/proconfig/widgets/language_models/claude_widgets.py index c39a2650..cfe97a2a 100755 --- a/proconfig/widgets/language_models/claude_widgets.py +++ b/proconfig/widgets/language_models/claude_widgets.py @@ -45,6 +45,7 @@ class MemoryItem(BaseModel): class ClaudeWidget(BaseWidget): NAME = "Claude 3.5 Sonnet" CATEGORY = "Large Language Model/Claude" + MYSHELL_WIDGET_NAME = "@myshell_llm/1744218088699596812" class InputsSchema(BaseWidget.InputsSchema): system_prompt: str = "" diff --git a/proconfig/widgets/myshell_widgets/__init__.py b/proconfig/widgets/myshell_widgets/__init__.py index cb961f94..5ea96e11 100755 --- a/proconfig/widgets/myshell_widgets/__init__.py +++ b/proconfig/widgets/myshell_widgets/__init__.py @@ -1,4 +1,5 @@ from proconfig.widgets.myshell_widgets.tools.image_text_fuser import ImageTextFuserWidget from proconfig.widgets.myshell_widgets.tools.image_canvas import ImageCanvasWidget from proconfig.widgets.myshell_widgets.tools.twitter_search import XWidget -from proconfig.widgets.myshell_widgets.tools.html2img import Html2ImgWidget \ No newline at end of file +from proconfig.widgets.myshell_widgets.tools.html2img import Html2ImgWidget +from proconfig.widgets.myshell_widgets.myshell_widget_caller import MyShellAnyWidgetCallerWidget \ No newline at end of file diff --git a/proconfig/widgets/myshell_widgets/myshell_widget_caller.py b/proconfig/widgets/myshell_widgets/myshell_widget_caller.py new file mode 100644 index 00000000..49f3c169 --- /dev/null +++ b/proconfig/widgets/myshell_widgets/myshell_widget_caller.py @@ -0,0 +1,90 @@ +from typing import Any, Literal, Optional, List +from pydantic import Field, BaseModel + +from proconfig.widgets.base import BaseWidget, WIDGETS +from proconfig.core.exception import ShellException + +# import instructor +import os +from pydantic import BaseModel, Field +import requests +import json + +@WIDGETS.register_module() +class MyShellAnyWidgetCallerWidget(BaseWidget): + NAME = "MyShellAnyWidgetCallerWidget" + # CATEGORY = "Myshell Widgets/Widget Caller" + dynamic_schema = True + + class InputsSchema(BaseWidget.InputsSchema): + widget_id: str = Field(..., description="the widget id to call") + inputs: dict = {} + + class OutputsSchema(BaseWidget.OutputsSchema): # useless + data: str | list + + def execute(self, environ, config): + # API endpoint URL + url = "https://openapi.myshell.ai/public/v1/widget/run" + widget_id = config["widget_id"] + # no myshell-test + + # Headers for the API request + headers = { + "x-myshell-openapi-key": os.environ["MYSHELL_API_KEY"], + "Content-Type": "application/json", + **environ.get("MYSHELL_HEADERS", {}) + } + + # Request payload + data = { + "widget_id": widget_id, + "input": json.dumps(config["inputs"]) + } + + # Send POST request to the API + response = requests.post(url, headers=headers, json=data) + + # Parse the JSON response + json_response = response.json() + + # Extract the 'result' field and return it as a string + if json_response.get('success') and 'result' in json_response: + result = json.loads(json_response['result']) + # handle the _url + if "_url" in result: + response = requests.get(result["_url"]) + if response.status_code == 200: + return response.json() + else: + return {"error_message": "error when retrieve the result"} + return result + else: + error = { + 'error_code': 'SHELL-1102', + 'error_head': 'Widget Execution Error', + 'msg': f'widget {widget_id} failed to execute', + 'traceback': json.dumps(json_response), + } + exception = ShellException(**error) + raise exception + + +if __name__ == "__main__": + from dotenv import load_dotenv + load_dotenv() + caller = MyShellAnyWidgetCallerWidget() + config = { + "widget_id": "1800948886988423168", + "inputs": { + "cfg_scale": 3.5, + "height": 1024, + "output_name": "result", + "prompt": "a beautiful girl", + "width": 1024 + } + } + config = caller.InputsSchema.model_validate(config) + + outputs = caller({}, config) + import pdb; pdb.set_trace() diff --git a/proconfig/widgets/myshell_widgets/tools/html2img.py b/proconfig/widgets/myshell_widgets/tools/html2img.py index 4dba3ff9..80657582 100644 --- a/proconfig/widgets/myshell_widgets/tools/html2img.py +++ b/proconfig/widgets/myshell_widgets/tools/html2img.py @@ -12,7 +12,8 @@ @WIDGETS.register_module() class Html2ImgWidget(BaseWidget): NAME = "HTML to Image" - CATEGORY = "Myshell Widgets/Tools" + CATEGORY = "Image Processing/HTML to Image" + MYSHELL_WIDGET_NAME = '@myshell/1850127954369142784' class InputsSchema(BaseWidget.InputsSchema): html_str: str = Field(default="", description="The HTML string to convert to an image") diff --git a/proconfig/widgets/myshell_widgets/tools/image_canvas.py b/proconfig/widgets/myshell_widgets/tools/image_canvas.py index 8c4215f3..11b36133 100644 --- a/proconfig/widgets/myshell_widgets/tools/image_canvas.py +++ b/proconfig/widgets/myshell_widgets/tools/image_canvas.py @@ -132,7 +132,7 @@ def get_next_image_filename(directory, prefix="ImageCanvas_", suffix=".png"): @WIDGETS.register_module() class ImageCanvasWidget(BaseWidget): NAME = "Image Canvas" - CATEGORY = 'Myshell Widgets/Tools' + CATEGORY = 'Image Processing/Image Canvas' class InputsSchema(BaseWidget.InputsSchema): config: str diff --git a/proconfig/widgets/myshell_widgets/tools/image_text_fuser.py b/proconfig/widgets/myshell_widgets/tools/image_text_fuser.py index 7b1c3981..1e2d3c1b 100755 --- a/proconfig/widgets/myshell_widgets/tools/image_text_fuser.py +++ b/proconfig/widgets/myshell_widgets/tools/image_text_fuser.py @@ -326,7 +326,7 @@ def add_content_to_image(template_path, content): @WIDGETS.register_module() class ImageTextFuserWidget(BaseWidget): NAME = "Image Text Fuser" - CATEGORY = 'Myshell Widgets/Tools' + # CATEGORY = 'Myshell Widgets/Tools' class InputsSchema(BaseWidget.InputsSchema): config: str = "" diff --git a/proconfig/widgets/myshell_widgets/tools/twitter_search.py b/proconfig/widgets/myshell_widgets/tools/twitter_search.py index d7eeae27..46247b4c 100644 --- a/proconfig/widgets/myshell_widgets/tools/twitter_search.py +++ b/proconfig/widgets/myshell_widgets/tools/twitter_search.py @@ -12,7 +12,8 @@ @WIDGETS.register_module() class XWidget(BaseWidget): NAME = "Twitter Search" - CATEGORY = "Myshell Widgets/Tools" + CATEGORY = "Tools/Twitter Search" + MYSHELL_WIDGET_NAME = '@myshell/1784206090390036480' class InputsSchema(BaseWidget.InputsSchema): action: Literal["search_tweets", "scrape_tweets", "scrape_profile"] = Field("scrape_tweets", description="The action to perform") diff --git a/proconfig/widgets/tools/code_runner.py b/proconfig/widgets/tools/code_runner.py index 8dfbe716..31e1abc8 100644 --- a/proconfig/widgets/tools/code_runner.py +++ b/proconfig/widgets/tools/code_runner.py @@ -5,7 +5,7 @@ @WIDGETS.register_module() class CodeRunnerWidget(BaseWidget): NAME = "Code Runner" - CATEGORY = "Tools/Code Runner" + # CATEGORY = "Tools/Code Runner" # hidden for now class InputsSchema(BaseWidget.InputsSchema): language: Literal["python", "javascript"] = "python" diff --git a/servers/automata.py b/servers/automata.py index b574eade..eb4854b6 100755 --- a/servers/automata.py +++ b/servers/automata.py @@ -18,8 +18,10 @@ from proconfig.widgets.tools.dependency_checker import check_dependency from proconfig.core import Automata +from proconfig.utils.expressions import calc_expression from proconfig.runners.runner import Runner from proconfig.utils.misc import convert_unserializable_display, process_local_file_path_async +from proconfig.utils.x_bot import convert_x_bot_to_automata from proconfig.utils.pytree import tree_map from proconfig.core.chat import ( ServerMessage, MessageComponentsContainer, MessageComponentsButton, @@ -404,7 +406,9 @@ class RuntimeData(BaseModel): trace_id_to_runtime_data_map: Dict[str, RuntimeData] = {} class MyShellUserInput(BaseModel): - type: str + type: str # "1": TEXT, "2": VOICE, "15": BUTTON_INTERACTION, "-1": 初始值, + # "20": X.MENTIONED, "21": "X.PINNED.REPLIED", "22": timer + form: Dict[str, Any] = {} button_id: str = "" text: str = "" @@ -412,21 +416,30 @@ class MyShellUserInput(BaseModel): def prepare_payload(automata: Automata, event_data: MyShellUserInput, sess_state: SessionState): # Decide the event_name - if event_data.type == "15": - event_name = event_data.button_id - elif event_data.type == "1": - event_name = "CHAT" - # Build the form data - event_data.form[CHAT_MESSAGE] = event_data.text - if len(event_data.embeded_objects) > 0: - # collect images - chat_images = [ - item["url"] for item in event_data.embeded_objects if item["type"] == "MESSAGE_METADATA_TYPE_IMAGE_FILE" - ] - if len(chat_images) > 0: - event_data.form[CHAT_IMAGES] = chat_images - else: - event_name = None + match event_data.type: + case "15": + event_name = event_data.button_id + case "1": + event_name = "CHAT" + # Build the form data + event_data.form[CHAT_MESSAGE] = event_data.text + if len(event_data.embeded_objects) > 0: + # collect images + chat_images = [ + item["url"] for item in event_data.embeded_objects if item["type"] == "MESSAGE_METADATA_TYPE_IMAGE_FILE" + ] + if len(chat_images) > 0: + event_data.form[CHAT_IMAGES] = chat_images + case "20" | "21": + event_name = "X.MENTIONED" if event_data.type == "20" else "X.PINNED.REPLIED" + ctx = {"payload": event_data.form} + target_inputs = getattr(sess_state.event_mapping[event_name], "target_inputs_transition", {}) + event_data.form = tree_map(lambda x: calc_expression(x, ctx), target_inputs) + case "22": + event_name = event_data.button_id + pass + case _: + event_name = None print("event_name:", event_name) target_state = automata.initial if event_name is None else sess_state.event_mapping[event_name].target_state @@ -873,7 +886,6 @@ async def run_automata_stateless(request: MyShellRunAppRequest): thread.start() thread.join() result = result_queue.get() - # result = run_automata_stateless_impl(request) return result @@ -907,4 +919,15 @@ async def get_intro_display(data: Dict): "text": data['server_message']['text'], "images": [item["url"] for item in data['server_message']['embedObjs'] if item["type"] == "MESSAGE_METADATA_TYPE_IMAGE_FILE"] } - return return_dict \ No newline at end of file + return return_dict + + +@app.post('/api/app/convert_x_bot') +async def convert_x_bot(data: Dict): + # convert x bot to normal automata + automata_x = data["automata_x"] + automata = convert_x_bot_to_automata(automata_x) + return { + "automata": automata + } + \ No newline at end of file diff --git a/servers/main.py b/servers/main.py index fe11f0d1..58f3fb87 100755 --- a/servers/main.py +++ b/servers/main.py @@ -23,6 +23,7 @@ import servers.settings import servers.comfy_runner import servers.helper + import servers.widgets diff --git a/servers/widgets.py b/servers/widgets.py new file mode 100644 index 00000000..42ed79fc --- /dev/null +++ b/servers/widgets.py @@ -0,0 +1,116 @@ +import json +from fastapi import FastAPI, HTTPException +from fastapi.responses import JSONResponse, StreamingResponse +from typing import Dict +from servers.base import app, APP_SAVE_ROOT, WORKFLOW_SAVE_ROOT, APP_RUNS_SAVE_ROOT, PROJECT_ROOT + +from proconfig.widgets.base import WIDGETS + +myshell_widget_list = json.load(open("assets/myshell_widget_list.json")) +@app.get("/api/widget/get_myshell_widget_list") +async def get_myshell_widget_list(): + # collect usage + usage_to_widget_map = {} + for widget_id, item in myshell_widget_list.items(): + item["widget_id"] = widget_id + usage = item["usage"] + if usage not in usage_to_widget_map: + usage_to_widget_map[usage] = [] + usage_to_widget_map[item["usage"]].append(item) + + data = [] + for usage, usage_widget_list in usage_to_widget_map.items(): + usage_data = { + "title": " ".join(word.capitalize() for word in usage.split("_")), + "items": [] + } + for widget_info in usage_widget_list: + widget_item = { + "name": "MyShellAnyWidgetCallerWidget", + "display_name": widget_info["name"], + "type": "widget", + "widget_name": f"@myshell/{widget_info['widget_id']}" + } + usage_data["items"].append(widget_item) + data.append(usage_data) + + response = { + "widget_list": data + } + return response + + +# @app.get("/api/widget/get_myshell_widget_schema") +def get_myshell_widget_schema(widget_id): + # widget_id = data["widget_id"] + response = { + "input_schema": myshell_widget_list[widget_id]["inputs"], + "output_schema": myshell_widget_list[widget_id]["outputs"], + } + return response + +@app.post("/api/workflow/get_widget_schema") +async def get_widget_schema(data: Dict[str, str]) -> Dict: + widget_name = data["widget_name"] + if widget_name == "MyShellAnyWidgetCallerWidget": + widget_id = data["myshell_widget_name"].split("/")[1] + print("get widget id:", widget_id) + return get_myshell_widget_schema(widget_id) + + WidgetClass = WIDGETS.module_dict[widget_name] + + response = {} + if len(WidgetClass.InputsSchemaDict) > 0: # multiple inputs schema + response["input_schema"] = { + k: v.model_json_schema() + for k, v in WidgetClass.InputsSchemaDict.items() + } + response["multi_input_schema"] = True + else: + response["input_schema"] = WidgetClass.InputsSchema.model_json_schema() + response["multi_input_schema"] = False + + response["output_schema"] = WidgetClass.OutputsSchema.model_json_schema() + + return response + + +@app.get("/api/widget/get_local_widget_list") +async def get_widget_category_list(): + # Step 1: Get categories + usage_to_categories_map = {} + + for category, widget_names in WIDGETS.category_dict.items(): + usage_name = category.split('/')[0] + category_name = category.split('/')[1] if isinstance(category, str) else category.value + if category_name == '': + continue + if usage_name not in usage_to_categories_map: + usage_to_categories_map[usage_name] = [] + + sub_widget_items = [ + { + "name": widget_name, + "display_name": WIDGETS._name_mapping.get(widget_name, "Warning: Display Name not Specified"), + "widget_name": getattr(WIDGETS._module_dict[widget_name], "MYSHELL_WIDGET_NAME", "") + } + for widget_name in widget_names + ] + + usage_to_categories_map[usage_name].append( + { + "name": category_name, + "children": sub_widget_items + } + ) + + response = { + "widget_list": [ + { + "title": usage, + "items": usage_to_categories_map[usage] + } + for usage in usage_to_categories_map + ] + } + return response \ No newline at end of file diff --git a/servers/workflow.py b/servers/workflow.py index c7535d85..cf083d46 100755 --- a/servers/workflow.py +++ b/servers/workflow.py @@ -84,25 +84,25 @@ async def get_widgets_by_category(data: Dict) -> Dict: } return response -@app.post("/api/workflow/get_widget_schema") -async def get_widget_schema(data: Dict[str, str]) -> Dict: - widget_name = data["widget_name"] - WidgetClass = WIDGETS.module_dict[widget_name] +# @app.post("/api/workflow/get_widget_schema") +# async def get_widget_schema(data: Dict[str, str]) -> Dict: +# widget_name = data["widget_name"] +# WidgetClass = WIDGETS.module_dict[widget_name] - response = {} - if len(WidgetClass.InputsSchemaDict) > 0: # multiple inputs schema - response["input_schema"] = { - k: v.model_json_schema() - for k, v in WidgetClass.InputsSchemaDict.items() - } - response["multi_input_schema"] = True - else: - response["input_schema"] = WidgetClass.InputsSchema.model_json_schema() - response["multi_input_schema"] = False +# response = {} +# if len(WidgetClass.InputsSchemaDict) > 0: # multiple inputs schema +# response["input_schema"] = { +# k: v.model_json_schema() +# for k, v in WidgetClass.InputsSchemaDict.items() +# } +# response["multi_input_schema"] = True +# else: +# response["input_schema"] = WidgetClass.InputsSchema.model_json_schema() +# response["multi_input_schema"] = False - response["output_schema"] = WidgetClass.OutputsSchema.model_json_schema() +# response["output_schema"] = WidgetClass.OutputsSchema.model_json_schema() - return response +# return response @app.post("/api/workflow/save") From 86311e308f598d7ed25153ee81077a9e65d0d565 Mon Sep 17 00:00:00 2001 From: shanexi Date: Wed, 15 Jan 2025 17:34:05 +0800 Subject: [PATCH 8/8] feat: add back Condition State --- web/apps/web/src/app/app/detail/page.tsx | 8 +++----- web/apps/web/src/stores/app/models/app-builder.model.ts | 5 +++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/apps/web/src/app/app/detail/page.tsx b/web/apps/web/src/app/app/detail/page.tsx index 63da99d5..8944de83 100644 --- a/web/apps/web/src/app/app/detail/page.tsx +++ b/web/apps/web/src/app/app/detail/page.tsx @@ -1,7 +1,6 @@ 'use client'; -import '../../reflect-metadata-client-side'; -import { FlowEngine, FlowRef, FlowAction } from '@shellagent/flow-engine'; +import { FlowAction, FlowEngine, FlowRef } from '@shellagent/flow-engine'; import { enableMapSet } from 'immer'; import { useInjection } from 'inversify-react'; import { observer } from 'mobx-react-lite'; @@ -10,14 +9,13 @@ import { useSearchParams } from 'next/navigation'; import { useEffect, useRef } from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import '../../reflect-metadata-client-side'; import { edgeTypes, - materialList, - xMaterialList, nodeTypes, - xNodeTypes, xEdgeTypes, + xNodeTypes, } from '@/components/app/constants'; import FlowHeader from '@/components/app/flow-header'; import { Header } from '@/components/app/header'; diff --git a/web/apps/web/src/stores/app/models/app-builder.model.ts b/web/apps/web/src/stores/app/models/app-builder.model.ts index 34863513..d6a743b7 100644 --- a/web/apps/web/src/stores/app/models/app-builder.model.ts +++ b/web/apps/web/src/stores/app/models/app-builder.model.ts @@ -99,6 +99,11 @@ const LocalWigets: MaterialListType = [ display_name: 'State', type: NodeTypeEnum.state, }, + { + name: 'Condition State', + display_name: 'Condition State', + type: NodeTypeEnum.condition_state, + }, { name: 'Note', display_name: 'Note',