├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .env ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── README.md │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── deps.py │ │ ├── router.py │ │ └── v1 │ │ │ ├── __init__.py │ │ │ ├── const │ │ │ └── common.py │ │ │ ├── endpoints │ │ │ ├── __init__.py │ │ │ ├── file.py │ │ │ ├── label_task.py │ │ │ ├── operator │ │ │ │ ├── __init__.py │ │ │ │ ├── label_task.py │ │ │ │ └── label_task_stat.py │ │ │ ├── team.py │ │ │ ├── team_invitation.py │ │ │ ├── team_member.py │ │ │ └── user.py │ │ │ └── router.py │ ├── client │ │ ├── __init__.py │ │ └── minio.py │ ├── core │ │ ├── __init__.py │ │ ├── config.py │ │ ├── exceptions.py │ │ └── security.py │ ├── crud │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crud_data.py │ │ ├── crud_file.py │ │ ├── crud_label_task.py │ │ ├── crud_record.py │ │ ├── crud_team.py │ │ ├── crud_team_invitation.py │ │ └── crud_user.py │ ├── db │ │ ├── __init__.py │ │ ├── init_db.py │ │ └── session.py │ ├── gunicorn_conf.py │ ├── logger │ │ ├── __init__.py │ │ └── logger.py │ ├── main.py │ ├── middleware │ │ ├── __init__.py │ │ └── middleware.py │ ├── models │ │ ├── __init__.py │ │ ├── data.py │ │ ├── file.py │ │ ├── label_task.py │ │ ├── record.py │ │ ├── team.py │ │ ├── team_invitation.py │ │ └── user.py │ ├── scheduler │ │ ├── __init__.py │ │ ├── init_scheduler.py │ │ └── task.py │ ├── schemas │ │ ├── __init__.py │ │ ├── data.py │ │ ├── evaluation.py │ │ ├── file.py │ │ ├── message.py │ │ ├── operator │ │ │ ├── __init__.py │ │ │ ├── stats.py │ │ │ └── task.py │ │ ├── record.py │ │ ├── task.py │ │ ├── team.py │ │ ├── tool.py │ │ └── user.py │ ├── tests │ │ └── __init__.py │ ├── util │ │ ├── __init__.py │ │ └── stats.py │ └── worker.py ├── migrations │ └── 2023_0615_1112_mig_team_member.py ├── pdm.lock ├── pdm.toml ├── pyproject.toml ├── scripts │ ├── start.sh │ └── worker.sh └── tests │ └── test.py ├── docker-compose.yaml ├── frontend ├── .commitlintrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .husky │ └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── Dockerfile ├── README.md ├── index.html ├── mock │ ├── auth │ │ └── auth.ts │ └── task │ │ └── taskList.ts ├── nginx.conf ├── package.json ├── postcss.config.js ├── public │ ├── favicon.svg │ └── iconfont_3732290.js ├── rollup.config.ts ├── scripts │ ├── bootstrap.js │ ├── generate_css_variables_from_antd_theme_token.js │ ├── generate_css_variables_from_antd_theme_token.ts │ └── guide.js ├── src │ ├── api │ │ ├── errorCode.ts │ │ ├── request.tsx │ │ ├── team.ts │ │ └── user.ts │ ├── apps │ │ ├── login │ │ │ ├── App.tsx │ │ │ ├── components │ │ │ │ └── FullPageScroll │ │ │ │ │ └── index.tsx │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ ├── pages │ │ │ │ └── login │ │ │ │ │ ├── bg.png │ │ │ │ │ └── index.tsx │ │ │ ├── readme.md │ │ │ ├── routes.tsx │ │ │ └── styles │ │ │ │ └── index.css │ │ ├── operator │ │ │ ├── App.tsx │ │ │ ├── README.md │ │ │ ├── assets │ │ │ │ ├── book.svg │ │ │ │ ├── calendar-finished.svg │ │ │ │ ├── calendar-progress.svg │ │ │ │ ├── calendar-total.svg │ │ │ │ ├── calendar.svg │ │ │ │ ├── demo-conversation@1x.png │ │ │ │ ├── demo-conversation@2x.png │ │ │ │ ├── demo-question@1x.png │ │ │ │ ├── demo-question@2x.png │ │ │ │ ├── demo-reply@1x.png │ │ │ │ ├── demo-reply@2x.png │ │ │ │ ├── diff.png │ │ │ │ ├── empty.svg │ │ │ │ ├── grid1.png │ │ │ │ ├── grid2.png │ │ │ │ ├── grid3.png │ │ │ │ ├── grid4.png │ │ │ │ ├── logo.svg │ │ │ │ ├── noAuth.svg │ │ │ │ ├── title.svg │ │ │ │ └── upload-cloud.svg │ │ │ ├── components │ │ │ │ ├── CopyTask.tsx │ │ │ │ ├── CustomEmpty.tsx │ │ │ │ ├── CustomFancy │ │ │ │ │ ├── QuestionEditor │ │ │ │ │ │ ├── Condition │ │ │ │ │ │ │ ├── RecursiveCondition │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── context.ts │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── TagSwitcher │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── svgs │ │ │ │ │ │ │ ├── delete.svg │ │ │ │ │ │ │ └── tree-switcher.svg │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── Help.tsx │ │ │ │ ├── JsonlUpload │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── jsonl.schema.json │ │ │ │ ├── NoAuth.tsx │ │ │ │ ├── PercentageCircle.tsx │ │ │ │ ├── QueryBlock.tsx │ │ │ │ ├── QueryForm.tsx │ │ │ │ ├── QueryTable.tsx │ │ │ │ ├── TableSelectedTips.tsx │ │ │ │ └── ToolConfigUpload │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── toolConfig.schema.json │ │ │ ├── constant │ │ │ │ ├── access.ts │ │ │ │ ├── chineseCharMap.ts │ │ │ │ └── query-key-factories │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── operator.ts │ │ │ │ │ ├── task.ts │ │ │ │ │ └── team.ts │ │ │ ├── hooks │ │ │ │ ├── useLockFn.ts │ │ │ │ ├── useScrollFetch.ts │ │ │ │ └── useUserInfo.ts │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ ├── layouts │ │ │ │ ├── CustomPageContainer.tsx │ │ │ │ ├── Main.tsx │ │ │ │ └── index.css │ │ │ ├── loaders │ │ │ │ ├── task.loader.ts │ │ │ │ └── team.loader.ts │ │ │ ├── pages │ │ │ │ ├── task.label.[id] │ │ │ │ │ ├── Analyze │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── DownloadRange.tsx │ │ │ │ │ ├── QuickCreate.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── left.tsx │ │ │ │ │ ├── right.tsx │ │ │ │ │ └── users.tsx │ │ │ │ ├── task.label.create │ │ │ │ │ ├── basic.tsx │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── tool.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── task.tsx │ │ │ │ ├── users.operator │ │ │ │ │ ├── Invite │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── users.team.[id] │ │ │ │ │ └── index.tsx │ │ │ │ └── users.team │ │ │ │ │ ├── Edit │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── routes.tsx │ │ │ ├── services │ │ │ │ ├── task.ts │ │ │ │ ├── team.ts │ │ │ │ └── types.ts │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── utils │ │ │ │ ├── bfsEach.ts │ │ │ │ ├── downloadFromUrl.ts │ │ │ │ ├── mapTree.ts │ │ │ │ └── objectEach.ts │ │ │ └── wrappers │ │ │ │ ├── CheckChildRoute.tsx │ │ │ │ ├── CheckUsersPagePermission.tsx │ │ │ │ └── LoginCheck.tsx │ │ └── supplier │ │ │ ├── App.tsx │ │ │ ├── README.md │ │ │ ├── assets │ │ │ ├── bg.png │ │ │ ├── empty.png │ │ │ ├── join.svg │ │ │ ├── logo.svg │ │ │ ├── preview.svg │ │ │ ├── timeout.svg │ │ │ └── title.svg │ │ │ ├── components │ │ │ ├── Copy │ │ │ │ └── index.tsx │ │ │ └── Empty │ │ │ │ └── index.tsx │ │ │ ├── constant │ │ │ ├── access.ts │ │ │ ├── operatorAccess.ts │ │ │ ├── query-key-factories │ │ │ │ ├── index.ts │ │ │ │ ├── member.ts │ │ │ │ └── task.ts │ │ │ └── task.ts │ │ │ ├── hooks │ │ │ ├── useTaskData.ts │ │ │ ├── useTaskParams.ts │ │ │ └── useUserInfo.ts │ │ │ ├── index.html │ │ │ ├── index.tsx │ │ │ ├── layouts │ │ │ ├── CustomPageContainer.tsx │ │ │ ├── Main.tsx │ │ │ └── index.css │ │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── join-team │ │ │ │ └── index.tsx │ │ │ ├── task.[id] │ │ │ │ ├── Answer │ │ │ │ │ └── index.tsx │ │ │ │ ├── AuditInfo │ │ │ │ │ └── index.tsx │ │ │ │ ├── ChatBox │ │ │ │ │ └── index.tsx │ │ │ │ ├── CheckTaskType │ │ │ │ │ └── index.tsx │ │ │ │ ├── Countdown │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomizeQuestion │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomizeTextarea │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── upload.ts │ │ │ │ ├── DiffModal │ │ │ │ │ └── index.tsx │ │ │ │ ├── Header │ │ │ │ │ └── index.tsx │ │ │ │ ├── PluginSet │ │ │ │ │ └── index.tsx │ │ │ │ ├── QuestionnaireSelect │ │ │ │ │ └── index.tsx │ │ │ │ ├── TaskForm │ │ │ │ │ └── index.tsx │ │ │ │ ├── Widget │ │ │ │ │ └── index.tsx │ │ │ │ ├── context.ts │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ └── task │ │ │ │ ├── Card │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── routes.tsx │ │ │ ├── services │ │ │ ├── joinTeam.ts │ │ │ └── task.ts │ │ │ ├── styles │ │ │ └── index.css │ │ │ └── wrappers │ │ │ ├── CheckPreviewAuth.tsx │ │ │ └── CheckTaskRouter.tsx │ ├── components │ │ ├── AppContainer.tsx │ │ ├── AppPanel │ │ │ ├── arrow.svg │ │ │ ├── index.tsx │ │ │ ├── labelu.svg │ │ │ ├── mineru.svg │ │ │ ├── odl.svg │ │ │ └── tool.svg │ │ ├── Breadcrumb.tsx │ │ ├── ErrorBoundary │ │ │ └── index.tsx │ │ ├── FancyGroup │ │ │ └── index.tsx │ │ ├── FancyInput │ │ │ ├── README.md │ │ │ ├── base │ │ │ │ ├── Boolean.fancy.tsx │ │ │ │ ├── Enum.fancy.tsx │ │ │ │ ├── Number.fancy.tsx │ │ │ │ └── String.fancy.tsx │ │ │ ├── fancyInput.ts │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── Help.tsx │ │ ├── IconFont │ │ │ └── index.tsx │ │ ├── Markdown │ │ │ ├── errorImage.png │ │ │ └── index.tsx │ │ ├── MemberInvite │ │ │ └── index.tsx │ │ ├── MessageBox │ │ │ └── index.tsx │ │ ├── RouterContainer.tsx │ │ └── StaticAnt.tsx │ ├── constant │ │ ├── chat.ts │ │ ├── query-key-factories │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── queryClient.tsx │ │ └── team.ts │ ├── hooks │ │ ├── useLang.ts │ │ └── useStoreIds.ts │ ├── initialize.tsx │ ├── loaders │ │ └── sso.loader.ts │ ├── locales │ │ ├── en-US.ts │ │ ├── index.ts │ │ └── zh-CN.ts │ ├── styles │ │ ├── font │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── global-variables.css │ │ ├── index.css │ │ └── theme.json │ ├── utils │ │ ├── getUrlExtension.ts │ │ ├── gid.ts │ │ ├── parseDocumentType.ts │ │ └── sso.ts │ ├── vite-env.d.ts │ └── wrappers │ │ └── RequireSSO.tsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.util.json ├── vite.config.dev.ts └── vite.config.prod.ts └── release-notes.md /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | frontend/node_modules 5 | .DS_Store 6 | 7 | # lock file 8 | frontend/yarn.lock 9 | frontend/index.html 10 | frontend/pnpm-lock.yaml 11 | gptcommit.toml 12 | .husky/prepare-commit-msg 13 | 14 | .idea 15 | 16 | # lock file 17 | yarn.lock 18 | # package-lock.json 19 | pnpm-lock.yaml 20 | 21 | # testing 22 | /coverage 23 | 24 | # production 25 | build 26 | dist 27 | es 28 | 29 | # misc 30 | .DS_Store 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | lerna-debug.log* 40 | 41 | /packages/server/public 42 | 43 | .vscode -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | ENVIRONMENT=local 3 | 4 | MINIO_ACCESS_KEY_ID = MekKrisWUnFFtsEk 5 | MINIO_ACCESS_KEY_SECRET = XK4uxD1czzYFJCRTcM70jVrchccBdy6C 6 | MINIO_ENDPOINT = localhost:9000 7 | MINIO_INTERNAL_ENDPOINT = minio:9000 8 | MINIO_BUCKET = label-llm-test 9 | 10 | MongoDB_DSN = mongodb://root:mypassword@mongo:27017 11 | MongoDB_DB_NAME = label_llm 12 | 13 | 14 | REDIS_DSN = redis://redis:6379/11 15 | 16 | SECRET_KEY="?*hsbRq5c9gpjBp~:oHU+7s8,I.67ewohfsib1=17dw@.q9r4Iidop:Oi_5oIYgw" -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM python:3.11 AS builder 3 | 4 | # set workdir 5 | WORKDIR /app 6 | 7 | # install PDM 8 | RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && pip install -U pip setuptools wheel && pip install pdm 9 | 10 | # copy files 11 | COPY pyproject.toml pdm.lock README.md /app/ 12 | 13 | # install dependencies and project into the local packages directory 14 | RUN pdm config pypi.url https://mirrors.aliyun.com/pypi/simple/ && mkdir __pypackages__ && pdm install --prod --no-lock --no-editable 15 | 16 | 17 | # run stage 18 | FROM python:3.11 19 | 20 | # set workdir 21 | WORKDIR /app 22 | 23 | # set env 24 | ARG APP_VERSION 25 | ENV PYTHONPATH=/app/pkgs 26 | ENV APP_VERSION ${APP_VERSION} 27 | 28 | RUN apt install libmagic1 29 | # retrieve packages from build stage 30 | COPY --from=builder /app/__pypackages__/3.11/lib /app/pkgs 31 | 32 | # copy files 33 | COPY ./ /app/ 34 | 35 | # set entrypoint 36 | CMD [ "sh", "/app/scripts/start.sh" ] -------------------------------------------------------------------------------- /backend/app/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 目录结构 3 | ``` 4 | ├── api 路由 5 | ├── client http客户端 6 | ├── core 设置、异常等系统基础项 7 | ├── crud crud抽象 8 | ├── db 数据库连接、配置 9 | ├── gunicorn_conf.py gunicorn配置 10 | ├── __init__.py __init__.py 11 | ├── logger 日志配置 12 | ├── main.py 程序入口 13 | ├── middleware 中间件 14 | ├── models 数据库模型 15 | ├── scheduler 定时任务 16 | ├── schemas 业务模型 17 | ├── tests 测试 18 | └── util 工具 19 | ``` -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/__init__.py -------------------------------------------------------------------------------- /backend/app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/api/__init__.py -------------------------------------------------------------------------------- /backend/app/api/deps.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, Request 2 | from loguru._logger import Logger 3 | 4 | from fastapi import Depends 5 | from fastapi.security import APIKeyCookie 6 | 7 | from app import crud, schemas 8 | from app.core import exceptions 9 | from app.core.security import verify_access_token 10 | 11 | oauth2_scheme = APIKeyCookie(name="access_token") 12 | 13 | 14 | async def get_logger( 15 | request: Request, 16 | ) -> Logger: 17 | return request.state.logger 18 | 19 | 20 | async def get_current_user(token: str = Depends(oauth2_scheme)): 21 | try: 22 | payload = verify_access_token(token) 23 | username = payload.get("sub") 24 | if username is None: 25 | raise exceptions.TOKEN_INVALID 26 | user = await crud.user.query(name=username).first_or_none() 27 | if user is None: 28 | raise exceptions.USER_NOT_EXIST 29 | except Exception as e: 30 | raise e 31 | return user 32 | 33 | 34 | async def get_current_team( 35 | user: schemas.user.DoUser = Depends(get_current_user), 36 | ): 37 | teams = await crud.team.query(user_id=user.user_id).to_list() 38 | 39 | return teams 40 | 41 | 42 | # 用户是管理员或运营 43 | async def is_admin_or_operator( 44 | user: schemas.user.DoUser = Depends(get_current_user), 45 | ) -> None: 46 | if user.role not in [ 47 | schemas.user.UserType.ADMIN, 48 | schemas.user.UserType.SUPER_ADMIN, 49 | ]: 50 | raise exceptions.USER_PERMISSION_DENIED 51 | 52 | return 53 | -------------------------------------------------------------------------------- /backend/app/api/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .v1.router import v1_router 4 | 5 | router = APIRouter() 6 | router.include_router(v1_router) 7 | 8 | 9 | @router.get("/health") 10 | async def health(): 11 | return {"status": "ok"} 12 | -------------------------------------------------------------------------------- /backend/app/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from .router import v1_router 2 | -------------------------------------------------------------------------------- /backend/app/api/v1/const/common.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from app import schemas 4 | from app.schemas.message import MessageBase 5 | 6 | 7 | def get_audit_review_record() -> schemas.operator.task.RespPreviewRecord: 8 | return schemas.operator.task.RespPreviewRecord( 9 | data_id=UUID("00000000-0000-0000-0000-000000000000"), 10 | prompt="这是伪数据,仅用于预览!", 11 | conversation=[ 12 | MessageBase( 13 | message_id=UUID("00000000-0000-0000-0000-000000000001"), 14 | parent_id=None, 15 | message_type="send", 16 | content="本数据仅用于预览!", 17 | ), 18 | MessageBase( 19 | message_id=UUID("00000000-0000-0000-0000-000000000002"), 20 | parent_id=UUID("00000000-0000-0000-0000-000000000001"), 21 | message_type="receive", 22 | content="本数据仅用于预览!", 23 | ), 24 | MessageBase( 25 | message_id=UUID("00000000-0000-0000-0000-000000000011"), 26 | parent_id=None, 27 | message_type="send", 28 | content="本数据仅用于预览!", 29 | ), 30 | MessageBase( 31 | message_id=UUID("00000000-0000-0000-0000-000000000012"), 32 | parent_id=UUID("00000000-0000-0000-0000-000000000011"), 33 | message_type="receive", 34 | content="本数据仅用于预览!", 35 | ), 36 | MessageBase( 37 | message_id=UUID("00000000-0000-0000-0000-000000000021"), 38 | parent_id=None, 39 | message_type="send", 40 | content="本数据仅用于预览!", 41 | ), 42 | MessageBase( 43 | message_id=UUID("00000000-0000-0000-0000-000000000022"), 44 | parent_id=UUID("00000000-0000-0000-0000-000000000021"), 45 | message_type="receive", 46 | content="本数据仅用于预览!", 47 | ), 48 | ], 49 | evaluation=schemas.evaluation.SingleEvaluation( 50 | message_evaluation=None, 51 | conversation_evaluation=None, 52 | questionnaire_evaluation=None, 53 | data_evaluation=None, 54 | ), 55 | reference_evaluation=None, 56 | label_user=None, 57 | ) 58 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/api/v1/endpoints/__init__.py -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/file.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import timedelta 3 | from pathlib import Path 4 | from io import BytesIO 5 | 6 | import magic 7 | import httpx 8 | from fastapi import APIRouter, Body, Depends, Response, UploadFile, File 9 | 10 | from app import crud, models, schemas 11 | from app.api import deps 12 | from app.client.minio import minio 13 | from app.core import exceptions 14 | from app.core.config import settings 15 | from app.db.session import redis_session 16 | 17 | router = APIRouter(prefix="/file", tags=["file"]) 18 | 19 | 20 | @router.post( 21 | "/file_upload", 22 | summary="上传文件", 23 | description="上传文件", 24 | ) 25 | async def file_upload( 26 | file: UploadFile, 27 | user: schemas.user.DoUser = Depends(deps.get_current_user), 28 | ): 29 | await redis_session.zremrangebyscore( 30 | f"file:upload:limit:{user.user_id}", 0, int(time.time()) - 60 31 | ) 32 | if await redis_session.zcard(f"file:upload:limit:{user.user_id}") > 60: 33 | raise exceptions.FILE_UPLOAD_LIMIT 34 | 35 | db_file = await crud.file.create( 36 | obj_in=models.file.FileCreate( 37 | creator_id=user.user_id, 38 | ) 39 | ) 40 | 41 | data = await file.read() 42 | 43 | minio.client.put_object( 44 | settings.MINIO_BUCKET, 45 | f"{settings.ENVIRONMENT}/file_upload/{db_file.file_id}{Path(file.filename or '').suffix}", 46 | BytesIO(data), 47 | length=len(data), 48 | ) 49 | 50 | return { 51 | "get_path": f"/api/v1/file/file_preview/{settings.ENVIRONMENT}/file_upload/{db_file.file_id}{Path(file.filename or '').suffix}", 52 | } 53 | 54 | 55 | @router.get( 56 | "/file_preview/{path:path}", 57 | summary="获取文件预览", 58 | description="获取文件预览", 59 | ) 60 | async def file_preview(path: str): 61 | url = minio.client.presigned_get_object( 62 | settings.MINIO_BUCKET, path, expires=timedelta(days=1) 63 | ) 64 | async with httpx.AsyncClient() as client: 65 | resp = await client.get(url) 66 | data = resp.content 67 | mimetype = magic.from_buffer(data[:1024], mime=True) 68 | return Response(data, media_type=mimetype) 69 | -------------------------------------------------------------------------------- /backend/app/api/v1/endpoints/operator/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | 3 | from app.api import deps 4 | 5 | from . import label_task, label_task_stat 6 | 7 | router = APIRouter( 8 | prefix="/operator", 9 | tags=["operator"], 10 | dependencies=[Depends(deps.is_admin_or_operator)], 11 | ) 12 | router.include_router(label_task.router) 13 | router.include_router(label_task_stat.router) 14 | 15 | -------------------------------------------------------------------------------- /backend/app/api/v1/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .endpoints import ( 4 | file, 5 | label_task, 6 | operator, 7 | team, 8 | team_invitation, 9 | team_member, 10 | user, 11 | ) 12 | 13 | v1_router = APIRouter(prefix="/v1") 14 | v1_router.include_router(label_task.router) 15 | v1_router.include_router(team.router) 16 | v1_router.include_router(team_invitation.router) 17 | v1_router.include_router(team_member.router) 18 | v1_router.include_router(operator.router) 19 | v1_router.include_router(user.router) 20 | v1_router.include_router(file.router) 21 | -------------------------------------------------------------------------------- /backend/app/client/__init__.py: -------------------------------------------------------------------------------- 1 | from . import minio 2 | -------------------------------------------------------------------------------- /backend/app/client/minio.py: -------------------------------------------------------------------------------- 1 | from minio import Minio 2 | 3 | from app.core.config import settings 4 | 5 | 6 | class MinioClient: 7 | def __init__( 8 | self, ak: str, sk: str, endpoint: str, internal_endpoint: str, bucket: str 9 | ): 10 | self.client = Minio(internal_endpoint, access_key=ak, secret_key=sk, secure=False) 11 | self.bucket = bucket 12 | self.endpoint = endpoint 13 | self.internal_endpoint = internal_endpoint 14 | 15 | minio = MinioClient( 16 | settings.MINIO_ACCESS_KEY_ID, 17 | settings.MINIO_ACCESS_KEY_SECRET, 18 | settings.MINIO_ENDPOINT, 19 | settings.MINIO_INTERNAL_ENDPOINT, 20 | settings.MINIO_BUCKET, 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /backend/app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/core/__init__.py -------------------------------------------------------------------------------- /backend/app/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import RedisDsn 2 | from pydantic_settings import BaseSettings 3 | import secrets 4 | 5 | 6 | class Settings(BaseSettings): 7 | # Debug Config 8 | DEBUG: bool = False 9 | 10 | # App Config 11 | APP_VERSION: str | None = None 12 | API_STR: str = "/api" 13 | ENVIRONMENT: str = "local" 14 | 15 | # MongoDB Config 16 | MongoDB_DSN: str = "" 17 | MongoDB_DB_NAME: str = "" 18 | 19 | # Redis Config 20 | REDIS_DSN: RedisDsn = RedisDsn("redis://localhost:16279/0") # type: ignore 21 | 22 | # Sentry Config 23 | SENTRY_DSN: str = "" 24 | 25 | # Minio Config 26 | MINIO_ACCESS_KEY_ID: str = "" 27 | MINIO_ACCESS_KEY_SECRET: str = "" 28 | MINIO_ENDPOINT: str = "" 29 | MINIO_INTERNAL_ENDPOINT: str = "" 30 | MINIO_BUCKET: str = "" 31 | 32 | SECRET_KEY: str = secrets.token_urlsafe(32) 33 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 34 | 35 | class Config: 36 | env_file = ".env" 37 | 38 | 39 | settings = Settings() 40 | -------------------------------------------------------------------------------- /backend/app/core/security.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from typing import Any 3 | 4 | from jose import jwt 5 | from passlib.context import CryptContext 6 | 7 | from app.core import exceptions 8 | from app.core.config import settings 9 | 10 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 11 | 12 | 13 | ALGORITHM = "HS256" 14 | 15 | 16 | def create_access_token(subject: str | Any, expires_delta: timedelta) -> str: 17 | expire = datetime.now(timezone.utc) + expires_delta 18 | to_encode = {"exp": expire, "sub": str(subject)} 19 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM) 20 | return encoded_jwt 21 | 22 | 23 | def verify_password(plain_password: str, hashed_password: str) -> bool: 24 | return pwd_context.verify(plain_password, hashed_password) 25 | 26 | 27 | def get_password_hash(password: str) -> str: 28 | return pwd_context.hash(password) 29 | 30 | 31 | def verify_access_token(token: str): 32 | try: 33 | payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) 34 | return payload 35 | except Exception as e: 36 | raise exceptions.TOKEN_INVALID 37 | -------------------------------------------------------------------------------- /backend/app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | from .crud_data import data 2 | from .crud_file import file 3 | from .crud_label_task import label_task 4 | from .crud_record import record 5 | from .crud_team import team 6 | from .crud_team_invitation import team_invitation_link 7 | from .crud_user import user 8 | -------------------------------------------------------------------------------- /backend/app/crud/crud_file.py: -------------------------------------------------------------------------------- 1 | from app.crud.base import CRUDBase 2 | from app.models.file import File, FileCreate, FileUpdate 3 | 4 | 5 | class CRUDFile(CRUDBase[File, FileCreate, FileUpdate]): 6 | ... 7 | 8 | 9 | file = CRUDFile(File) 10 | -------------------------------------------------------------------------------- /backend/app/crud/crud_label_task.py: -------------------------------------------------------------------------------- 1 | import re 2 | from uuid import UUID 3 | 4 | from beanie.operators import ElemMatch, In, RegEx 5 | 6 | from app import schemas 7 | from app.crud.base import CRUDBase 8 | from app.models.label_task import LabelTask, LabelTaskCreate, LabelTaskUpdate 9 | 10 | 11 | class CRUDLabelTask(CRUDBase[LabelTask, LabelTaskCreate, LabelTaskUpdate]): 12 | def query( 13 | self, 14 | *, 15 | _id: list[str] | str | None = None, 16 | skip: int | None = None, 17 | limit: int | None = None, 18 | sort: str | list[str] | None = None, 19 | title: str | None = None, 20 | status: schemas.task.TaskStatus | list[schemas.task.TaskStatus] | None = None, 21 | team_id: UUID | list[UUID] | None = None, 22 | task_id: UUID | list[UUID] | None = None, 23 | creator_id: str | list[str] | None = None, 24 | ): 25 | query = super().query(_id=_id, sort=sort, skip=skip, limit=limit) 26 | 27 | if title is not None and len(title.strip()) > 0: 28 | title = title.strip() 29 | query = query.find(RegEx(field=self.model.title, pattern=re.escape(title), options="i")) # type: ignore 30 | 31 | if status is not None: 32 | if isinstance(status, list): 33 | query = query.find(In(self.model.status, status)) 34 | else: 35 | query = query.find(self.model.status == status) 36 | 37 | if team_id is not None: 38 | if isinstance(team_id, list): 39 | query = query.find(ElemMatch(self.model.teams, {"$in": team_id})) 40 | else: 41 | query = query.find(ElemMatch(self.model.teams, {"$eq": team_id})) 42 | 43 | if task_id is not None: 44 | if isinstance(task_id, list): 45 | query = query.find(In(self.model.task_id, task_id)) 46 | else: 47 | query = query.find(self.model.task_id == task_id) 48 | 49 | if creator_id is not None: 50 | if isinstance(creator_id, list): 51 | query = query.find(In(self.model.creator_id, creator_id)) 52 | else: 53 | query = query.find(self.model.creator_id == creator_id) 54 | 55 | return query 56 | 57 | 58 | label_task = CRUDLabelTask(LabelTask) 59 | -------------------------------------------------------------------------------- /backend/app/crud/crud_team.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from uuid import UUID 3 | 4 | from beanie.operators import Eq, In, RegEx 5 | 6 | from app.crud.base import CRUDBase 7 | from app.models.team import Team, TeamCreate, TeamUpdate 8 | 9 | 10 | class CRUDTeam(CRUDBase[Team, TeamCreate, TeamUpdate]): 11 | def query( 12 | self, 13 | *, 14 | _id: list[Any] | Any = None, 15 | skip: int | None = None, 16 | limit: int | None = None, 17 | sort: str | list[str] | None = None, 18 | user_id: list[str] | str | None = None, 19 | team_id: list[UUID] | UUID | None = None, 20 | name: str | None = None, 21 | ): 22 | query = super().query(_id=_id, skip=skip, limit=limit, sort=sort) 23 | 24 | if user_id is not None: 25 | if isinstance(user_id, list): 26 | query = query.find(In("users.user_id", user_id)) 27 | else: 28 | query = query.find(Eq("users.user_id", user_id)) 29 | if team_id is not None: 30 | if isinstance(team_id, list): 31 | query = query.find(In(self.model.team_id, team_id)) 32 | else: 33 | query = query.find(self.model.team_id == team_id) 34 | 35 | if name is not None: 36 | query = query.find(RegEx(field=self.model.name, pattern=name)) # type: ignore 37 | 38 | return query 39 | 40 | 41 | team = CRUDTeam(Team) 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/app/crud/crud_team_invitation.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from uuid import UUID 3 | 4 | from app.crud.base import CRUDBase 5 | from app.models.team_invitation import ( 6 | TeamInvitationLink, 7 | TeamInvitationLinkCreate, 8 | TeamInvitationLinkUpdate, 9 | ) 10 | 11 | 12 | class CRUDTeamInvitationLink( 13 | CRUDBase[TeamInvitationLink, TeamInvitationLinkCreate, TeamInvitationLinkUpdate] 14 | ): 15 | def query( 16 | self, 17 | *, 18 | _id: list[Any] | Any = None, 19 | skip: int | None = None, 20 | limit: int | None = None, 21 | sort: str | list[str] | None = None, 22 | link_id: UUID | None = None, 23 | ): 24 | query = super().query(_id=_id, sort=sort, skip=skip, limit=limit) 25 | 26 | if link_id is not None: 27 | query = query.find(self.model.link_id == link_id) 28 | 29 | return query 30 | 31 | 32 | team_invitation_link = CRUDTeamInvitationLink(TeamInvitationLink) 33 | -------------------------------------------------------------------------------- /backend/app/crud/crud_user.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from beanie.operators import In, RegEx 4 | 5 | from app.crud.base import CRUDBase 6 | from app.models.user import User, UserCreate, UserUpdate 7 | from app.schemas.user import UserType 8 | 9 | 10 | class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): 11 | def query( 12 | self, 13 | *, 14 | _id: list[Any] | Any = None, 15 | skip: int | None = None, 16 | limit: int | None = None, 17 | sort: str | list[str] | None = None, 18 | user_id: list[str] | str | None = None, 19 | name: str | None = None, 20 | role: list[UserType] | UserType | None = None, 21 | password: list[str] | str | None = None 22 | ): 23 | query = super().query(_id=_id, sort=sort, skip=skip, limit=limit) 24 | 25 | if user_id is not None: 26 | if isinstance(user_id, list): 27 | query = query.find(In(self.model.user_id, user_id)) 28 | else: 29 | query = query.find(self.model.user_id == user_id) 30 | 31 | if role: 32 | if isinstance(role, list): 33 | query = query.find(In(self.model.role, role)) 34 | else: 35 | query.find(self.model.role==role) 36 | 37 | if password: 38 | if isinstance(password, list): 39 | query = query.find(In(self.model.password, password)) 40 | else: 41 | query = query.find(self.model.password == password) 42 | 43 | if name: 44 | query = query.find(RegEx(field=self.model.name, pattern=name)) # type: ignore 45 | 46 | return query 47 | 48 | 49 | user = CRUDUser(User) 50 | -------------------------------------------------------------------------------- /backend/app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/db/__init__.py -------------------------------------------------------------------------------- /backend/app/db/init_db.py: -------------------------------------------------------------------------------- 1 | from beanie import init_beanie 2 | 3 | from app import crud, schemas 4 | from app.core.config import settings 5 | from app.db.session import mongo_session, redis_session 6 | from app.models.data import Data 7 | from app.models.file import File 8 | from app.models.label_task import LabelTask 9 | from app.models.record import Record 10 | from app.models.team import Team, TeamCreate 11 | from app.models.team_invitation import TeamInvitationLink 12 | from app.models.user import User 13 | from app.schemas.team import TeamMember, TeamMemberRole 14 | 15 | 16 | async def init_db(): 17 | await init_beanie( 18 | database=mongo_session[settings.MongoDB_DB_NAME], 19 | document_models=[User, LabelTask, Data, Team, TeamInvitationLink, Record, File], # type: ignore 20 | ) 21 | 22 | # 创建默认团队 23 | team = await crud.team.query(team_id=schemas.team.DEFAULT_TEAM_ID).first_or_none() 24 | if not team: 25 | team = await crud.team.create( 26 | obj_in=TeamCreate( 27 | team_id=schemas.team.DEFAULT_TEAM_ID, 28 | name="默认团队", 29 | owner="", 30 | owner_cellphone="", 31 | ) 32 | ) 33 | users = await crud.user.query().to_list() 34 | team.users = [ 35 | TeamMember(user_id=user.user_id, name="", role=TeamMemberRole.USER) 36 | for user in users 37 | ] 38 | team.user_count = len(users) 39 | await team.save() # type: ignore 40 | 41 | await redis_session.ping() 42 | 43 | 44 | async def close_db(): 45 | mongo_session.close() 46 | await redis_session.close() 47 | -------------------------------------------------------------------------------- /backend/app/db/session.py: -------------------------------------------------------------------------------- 1 | from motor import motor_asyncio 2 | from redis.asyncio import Redis 3 | 4 | from app.core.config import settings 5 | 6 | # mongo session 7 | mongo_session = motor_asyncio.AsyncIOMotorClient(settings.MongoDB_DSN) 8 | 9 | # redis session 10 | redis_session = Redis.from_url(str(settings.REDIS_DSN)) 11 | 12 | -------------------------------------------------------------------------------- /backend/app/gunicorn_conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import multiprocessing 3 | import os 4 | 5 | workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") 6 | max_workers_str = os.getenv("MAX_WORKERS") 7 | use_max_workers = None 8 | if max_workers_str: 9 | use_max_workers = int(max_workers_str) 10 | web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) 11 | 12 | host = os.getenv("HOST", "0.0.0.0") 13 | port = os.getenv("PORT", "8080") 14 | bind_env = os.getenv("BIND", None) 15 | use_loglevel = os.getenv("LOG_LEVEL", "info") 16 | if bind_env: 17 | use_bind = bind_env 18 | else: 19 | use_bind = f"{host}:{port}" 20 | 21 | cores = multiprocessing.cpu_count() 22 | workers_per_core = float(workers_per_core_str) 23 | default_web_concurrency = workers_per_core * cores 24 | if web_concurrency_str: 25 | web_concurrency = int(web_concurrency_str) 26 | assert web_concurrency > 0 27 | else: 28 | web_concurrency = max(int(default_web_concurrency), 2) 29 | if use_max_workers: 30 | web_concurrency = min(web_concurrency, use_max_workers) 31 | accesslog_var = os.getenv("ACCESS_LOG", "-") 32 | use_accesslog = accesslog_var or None 33 | errorlog_var = os.getenv("ERROR_LOG", "-") 34 | use_errorlog = errorlog_var or None 35 | graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120") 36 | timeout_str = os.getenv("TIMEOUT", "120") 37 | keepalive_str = os.getenv("KEEP_ALIVE", "5") 38 | 39 | # Gunicorn config variables 40 | loglevel = use_loglevel 41 | workers = 1 42 | bind = use_bind 43 | errorlog = use_errorlog 44 | worker_tmp_dir = "/dev/shm" 45 | accesslog = use_accesslog 46 | graceful_timeout = int(graceful_timeout_str) 47 | timeout = int(timeout_str) 48 | keepalive = int(keepalive_str) 49 | 50 | 51 | # For debugging and testing 52 | log_data = { 53 | "loglevel": loglevel, 54 | "workers": workers, 55 | "bind": bind, 56 | "graceful_timeout": graceful_timeout, 57 | "timeout": timeout, 58 | "keepalive": keepalive, 59 | "errorlog": errorlog, 60 | "accesslog": accesslog, 61 | # Additional, non-gunicorn variables 62 | "workers_per_core": workers_per_core, 63 | "use_max_workers": use_max_workers, 64 | "host": host, 65 | "port": port, 66 | } 67 | print(json.dumps(log_data)) 68 | -------------------------------------------------------------------------------- /backend/app/logger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/logger/__init__.py -------------------------------------------------------------------------------- /backend/app/logger/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from loguru import logger 4 | 5 | 6 | def init_logger(): 7 | logger.remove() 8 | 9 | # scheduler log 10 | logger.add( 11 | sys.stdout, 12 | colorize=True, 13 | format="{time:YYYY-MM-DD HH:mm:ss:SSS zz} | scheduler | {level} | {message}", 14 | filter="app.scheduler", 15 | ) 16 | 17 | # web middleware log 18 | logger.add( 19 | sys.stdout, 20 | colorize=True, 21 | format="{time:YYYY-MM-DD HH:mm:ss:SSS zz} | middleware | {level} | {extra[request_id]} | {message}", 22 | filter="app.middleware", 23 | ) 24 | 25 | # web log 26 | logger.add( 27 | sys.stdout, 28 | colorize=True, 29 | format="{time:YYYY-MM-DD HH:mm:ss:SSS zz} | server | {level} | {extra[request_id]} | {message}", 30 | filter="app.api", 31 | ) 32 | -------------------------------------------------------------------------------- /backend/app/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import asynccontextmanager 3 | 4 | from fastapi import FastAPI 5 | 6 | from app.api.router import router 7 | from app.core.config import settings 8 | from app.db.init_db import close_db, init_db 9 | from app.logger.logger import init_logger 10 | from app.scheduler.init_scheduler import scheduler 11 | 12 | 13 | @asynccontextmanager 14 | async def lifespan(app: FastAPI): 15 | try: 16 | # 初始化日志 17 | init_logger() 18 | # 初始化数据库 19 | await init_db() 20 | # 初始化定时任务 21 | scheduler.start() 22 | except Exception as e: 23 | raise e 24 | yield 25 | # 关闭定时任务 26 | scheduler.shutdown() 27 | # 关闭数据库 28 | await close_db() 29 | 30 | 31 | app = FastAPI( 32 | debug=settings.DEBUG, 33 | lifespan=lifespan, 34 | docs_url="/docs" if settings.DEBUG else None, 35 | redoc_url="/redoc" if settings.DEBUG else None, 36 | openapi_url="/api/openapi.json" if settings.DEBUG else None, 37 | ) 38 | 39 | 40 | app.include_router(router, prefix=settings.API_STR) 41 | 42 | 43 | if __name__ == "__main__": 44 | import uvicorn 45 | 46 | uvicorn.run(app, host="0.0.0.0", port=8080) 47 | -------------------------------------------------------------------------------- /backend/app/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/middleware/__init__.py -------------------------------------------------------------------------------- /backend/app/middleware/middleware.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import uuid4 3 | 4 | from fastapi import FastAPI, Request 5 | from loguru import logger 6 | from starlette.types import Message 7 | 8 | 9 | async def set_body(_receive: Message): 10 | async def receive() -> Message: 11 | return _receive 12 | 13 | return receive 14 | 15 | 16 | def init_middleware(app: FastAPI): 17 | app.middleware("http")(call_info) 18 | app.middleware("http")(add_request_id_logger) 19 | app.middleware("http")(add_request_id) 20 | 21 | 22 | async def add_request_id(request: Request, call_next): 23 | request.state.request_id = request.headers.get("X-Request-Id", None) 24 | if not request.state.request_id: 25 | request.state.request_id = uuid4().hex 26 | 27 | response = await call_next(request) 28 | response.headers["X-Request-Id"] = request.state.request_id 29 | return response 30 | 31 | 32 | async def add_request_id_logger(request: Request, call_next): 33 | request.state.logger = logger.bind(request_id=request.state.request_id) 34 | response = await call_next(request) 35 | return response 36 | 37 | 38 | async def call_info( 39 | request: Request, 40 | call_next, 41 | ): 42 | # request start 43 | request.state.logger.info( 44 | "request start | {} | {}", 45 | request.method, 46 | request.url.path, 47 | ) 48 | 49 | # 小于1M的请求打印body 50 | content_length = request.headers.get("Content-Length", "") 51 | if content_length.isdigit() and int(content_length) < 4096: 52 | body = await request._receive() 53 | request.state.logger.info( 54 | "request body | {}", 55 | body["body"].decode(), 56 | ) 57 | request._receive = await set_body(body) 58 | 59 | start_time = datetime.utcnow() 60 | 61 | response = await call_next(request) 62 | 63 | process_time = (datetime.utcnow() - start_time).microseconds / 1000 64 | 65 | # user info 66 | request.state.logger.info( 67 | "user info | user_id: {}", 68 | request.state.user_id if hasattr(request.state, "user_id") else None, 69 | ) 70 | 71 | # response info 72 | request.state.logger.info( 73 | "request end | process time: {}ms | status code: {}", 74 | process_time, 75 | response.status_code, 76 | ) 77 | 78 | return response 79 | -------------------------------------------------------------------------------- /backend/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import data, file, label_task, record, team, team_invitation, user 2 | -------------------------------------------------------------------------------- /backend/app/models/data.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | from uuid import UUID, uuid4 4 | 5 | from beanie import Document, Indexed 6 | from pydantic import BaseModel, Field 7 | 8 | from app import schemas 9 | 10 | 11 | class Data(Document): 12 | # 数据id 13 | data_id: Annotated[UUID, Indexed(unique=True)] 14 | 15 | # 数据来源id 16 | source_data_id: Annotated[UUID | None, Indexed] = Field(default=None) 17 | 18 | # 结果id 19 | result_id: UUID 20 | 21 | # 任务id 22 | task_id: Annotated[UUID, Indexed()] 23 | 24 | # 数据状态 25 | status: schemas.data.DataStatus 26 | 27 | # 问卷id 28 | questionnaire_id: Annotated[UUID, Indexed()] 29 | 30 | # 提示内容 31 | prompt: str 32 | 33 | # 对话id 34 | conversation_id: UUID 35 | 36 | # 对话内容 37 | conversation: list[schemas.message.Message] 38 | 39 | # 参考评价 40 | reference_evaluation: schemas.evaluation.LabelEvaluation | None = Field(default=None) 41 | 42 | # 评价 43 | evaluation: schemas.evaluation.Evaluation 44 | 45 | # 更新时间 46 | update_time: int 47 | 48 | # 自定义数据 49 | custom: dict = Field(default_factory=dict) 50 | 51 | # 是否被抽样过 52 | sampled: bool | None = Field(default=None) 53 | 54 | class Settings: 55 | use_revision = True 56 | 57 | 58 | class DataCreate(BaseModel): 59 | data_id: UUID = Field(default_factory=uuid4) 60 | source_data_id: UUID | None = None 61 | result_id: UUID = Field(default_factory=uuid4) 62 | task_id: UUID 63 | status: schemas.data.DataStatus = schemas.data.DataStatus.PENDING 64 | questionnaire_id: UUID 65 | prompt: str 66 | conversation_id: UUID 67 | conversation: list[schemas.message.Message] 68 | reference_evaluation: schemas.evaluation.LabelEvaluation | None 69 | evaluation: schemas.evaluation.Evaluation = Field( 70 | default_factory=schemas.evaluation.Evaluation 71 | ) 72 | update_time: int = Field(default_factory=lambda: int(time.time())) 73 | custom: dict = Field(default_factory=dict) 74 | 75 | 76 | class DataUpdate(BaseModel): 77 | status: schemas.data.DataStatus | None = None 78 | evaluation: schemas.evaluation.Evaluation | None = None 79 | update_time: int = Field(default_factory=lambda: int(time.time())) 80 | -------------------------------------------------------------------------------- /backend/app/models/file.py: -------------------------------------------------------------------------------- 1 | import time 2 | from uuid import UUID, uuid4 3 | 4 | from beanie import Document 5 | from pydantic import BaseModel, Field 6 | 7 | 8 | class File(Document): 9 | file_id: UUID 10 | creator_id: str 11 | create_time: int 12 | 13 | 14 | class FileCreate(BaseModel): 15 | file_id: UUID = Field(default_factory=uuid4) 16 | creator_id: str 17 | create_time: int = Field(default_factory=lambda: int(time.time())) 18 | 19 | 20 | class FileUpdate(BaseModel): 21 | pass 22 | -------------------------------------------------------------------------------- /backend/app/models/label_task.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | from uuid import UUID, uuid4 4 | 5 | from beanie import Document, Indexed 6 | from pydantic import BaseModel, Field 7 | 8 | from app import schemas 9 | 10 | 11 | class LabelTask(Document): 12 | """ 13 | 标注任务 14 | """ 15 | 16 | # 任务id 17 | task_id: Annotated[UUID, Indexed(unique=True)] 18 | 19 | # 任务标题 20 | title: str 21 | # 任务描述 22 | description: str 23 | 24 | # 创建时间 25 | create_time: int 26 | # 创建人ID 27 | creator_id: str 28 | 29 | # 任务状态 30 | status: schemas.task.TaskStatus 31 | 32 | # 工具配置 33 | tool_config: dict 34 | 35 | # 问卷分发次数 36 | distribute_count: int 37 | 38 | # 问卷答题限制时间 39 | expire_time: int 40 | 41 | # 执行团队 42 | teams: list[UUID] 43 | 44 | class Settings: 45 | use_revision = True 46 | 47 | 48 | class LabelTaskCreate(BaseModel): 49 | """ 50 | 标注任务创建 51 | """ 52 | 53 | task_id: UUID = Field(default_factory=uuid4) 54 | title: str 55 | description: str 56 | create_time: int = Field(default_factory=lambda: int(time.time())) 57 | creator_id: str 58 | status: schemas.task.TaskStatus = schemas.task.TaskStatus.CREATED 59 | tool_config: dict 60 | distribute_count: int 61 | expire_time: int = 0 62 | teams: list[UUID] = Field(default_factory=list) 63 | 64 | 65 | class LabelTaskUpdate(BaseModel): 66 | """ 67 | 标注任务更新 68 | """ 69 | 70 | title: str | None = Field(default=None) 71 | description: str | None = Field(default=None) 72 | status: schemas.task.TaskStatus | None = Field(default=None) 73 | tool_config: dict | None = Field(default=None) 74 | distribute_count: int | None = Field(default=None) 75 | expire_time: int | None = Field(default=None) 76 | teams: list[UUID] | None = Field(default=None) 77 | -------------------------------------------------------------------------------- /backend/app/models/record.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | from uuid import UUID 4 | 5 | from beanie import Document, Indexed 6 | from pydantic import BaseModel, Field 7 | 8 | from app import schemas 9 | 10 | 11 | # 数据提交记录 12 | class Record(Document): 13 | # 数据id 14 | data_id: Annotated[UUID, Indexed()] 15 | 16 | # 流程索引 17 | flow_index: int = 1 18 | 19 | # 任务id 20 | task_id: Annotated[UUID, Indexed()] 21 | 22 | # 问卷id 23 | questionnaire_id: UUID 24 | 25 | # 创建人id 26 | creator_id: Annotated[str, Indexed()] 27 | 28 | # 创建时间 29 | create_time: int 30 | 31 | # 提交时间 32 | submit_time: int | None = Field(default=None) 33 | 34 | # 评价 35 | evaluation: schemas.evaluation.Evaluation | None = Field(default=None) 36 | 37 | # 提交的结果状态 38 | status: schemas.record.RecordStatus = schemas.record.RecordStatus.COMPLETED 39 | 40 | class Settings: 41 | use_revision = True 42 | 43 | 44 | class RecordCreate(BaseModel): 45 | data_id: UUID 46 | flow_index: int = 1 47 | task_id: UUID 48 | questionnaire_id: UUID 49 | creator_id: str 50 | create_time: int = Field(default_factory=lambda: int(time.time())) 51 | submit_time: int | None = Field(default=None) 52 | evaluation: schemas.evaluation.Evaluation | None = Field(default=None) 53 | status: schemas.record.RecordStatus = schemas.record.RecordStatus.PROCESSING 54 | 55 | 56 | class RecordUpdate(BaseModel): 57 | submit_time: int | None = Field(default=None) 58 | evaluation: schemas.evaluation.Evaluation | None = Field(default=None) 59 | status: schemas.record.RecordStatus | None = Field(default=None) 60 | -------------------------------------------------------------------------------- /backend/app/models/team.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | from uuid import UUID, uuid4 4 | 5 | from beanie import Document, Indexed 6 | from pydantic import BaseModel, Field 7 | from app.schemas.team import TeamMember 8 | 9 | class Team(Document): 10 | # id 11 | team_id: Annotated[UUID, Indexed(unique=True)] 12 | 13 | # 名字 14 | name: str 15 | 16 | # 联系人 17 | owner: str | None = Field(default=None) 18 | 19 | # 联系人电话 20 | owner_cellphone: str | None = Field(default=None) 21 | 22 | # 创建时间 23 | create_time: int 24 | 25 | # 更新时间 26 | update_time: int 27 | 28 | # 用户 id 列表 29 | users: list[TeamMember] = Field(default_factory=list) 30 | 31 | # 成员个数 32 | user_count: int 33 | 34 | 35 | class TeamCreate(BaseModel): 36 | team_id: UUID = Field(default_factory=uuid4) 37 | name: str 38 | owner: str | None = Field(default=None) 39 | owner_cellphone: str | None = Field(default=None) 40 | create_time: int = Field(default_factory=lambda: int(time.time())) 41 | update_time: int = Field(default_factory=lambda: int(time.time())) 42 | users: list[TeamMember] = Field(default_factory=list) 43 | user_count: int = Field(default=0) 44 | 45 | 46 | class TeamUpdate(BaseModel): 47 | name: str | None = Field(default=None) 48 | owner: str | None = Field(default=None) 49 | owner_cellphone: str | None = Field(default=None) 50 | users: list[TeamMember] | None = Field(default=None) 51 | user_count: int | None = Field(default=None) 52 | update_time: int = Field(default_factory=lambda: int(time.time())) 53 | -------------------------------------------------------------------------------- /backend/app/models/team_invitation.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | from uuid import UUID, uuid4 4 | 5 | from beanie import Document, Indexed 6 | from pydantic import BaseModel, Field 7 | 8 | 9 | ## InvitaionLink 10 | class TeamInvitationLink(Document): 11 | # link id 12 | link_id: Annotated[UUID, Indexed(unique=True)] 13 | 14 | # team id 15 | team_id: UUID 16 | 17 | # 创建时间 18 | create_time: int 19 | # 更新时间 20 | expire_time: int 21 | 22 | 23 | class TeamInvitationLinkCreate(BaseModel): 24 | link_id: UUID = Field(default_factory=uuid4) 25 | team_id: UUID 26 | create_time: int = Field(default_factory=lambda: int(time.time())) 27 | expire_time: int = Field(default_factory=lambda: int(time.time()) + 12 * 60 * 60) 28 | 29 | 30 | class TeamInvitationLinkUpdate(BaseModel): ... 31 | -------------------------------------------------------------------------------- /backend/app/models/user.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Annotated 3 | 4 | from beanie import Document, Indexed 5 | from pydantic import BaseModel, Field 6 | 7 | from app import schemas 8 | 9 | 10 | class User(Document): 11 | # 用户id, 目前使用sso的id 12 | user_id: Annotated[str, Indexed(unique=True)] 13 | 14 | # 用户密码 15 | password: str 16 | 17 | # 用户角色 18 | role: schemas.user.UserType 19 | 20 | # 用户名称 21 | name: str 22 | 23 | # 创建时间 24 | create_time: int = 0 25 | 26 | # 更新时间 27 | update_time: int = 0 28 | 29 | 30 | class UserCreate(BaseModel): 31 | user_id: str 32 | name: str 33 | password: str 34 | role: schemas.user.UserType = schemas.user.UserType.USER 35 | create_time: int = Field(default_factory=lambda: int(time.time())) 36 | update_time: int = Field(default_factory=lambda: int(time.time())) 37 | 38 | 39 | class UserUpdate(BaseModel): 40 | name: str | None = Field(default=None) 41 | role: schemas.user.UserType | None = Field(default=None) 42 | update_time: int = Field(default_factory=lambda: int(time.time())) 43 | -------------------------------------------------------------------------------- /backend/app/scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | from .init_scheduler import scheduler 2 | -------------------------------------------------------------------------------- /backend/app/scheduler/init_scheduler.py: -------------------------------------------------------------------------------- 1 | from apscheduler.executors.asyncio import AsyncIOExecutor 2 | from apscheduler.jobstores.redis import RedisJobStore 3 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 4 | 5 | from app.core.config import settings 6 | 7 | jobstores = { 8 | "default": RedisJobStore( 9 | host=settings.REDIS_DSN.host, 10 | port=settings.REDIS_DSN.port, 11 | db=int(settings.REDIS_DSN.path[1:] if settings.REDIS_DSN.path else 0), 12 | password=settings.REDIS_DSN.password, 13 | ) 14 | } 15 | 16 | executors = {"default": AsyncIOExecutor()} 17 | 18 | job_defaults = {"coalesce": True, "max_instances": 1} 19 | 20 | scheduler = AsyncIOScheduler( 21 | jobstores=jobstores, executors=executors, job_defaults=job_defaults 22 | ) 23 | -------------------------------------------------------------------------------- /backend/app/scheduler/task.py: -------------------------------------------------------------------------------- 1 | import time 2 | from uuid import UUID 3 | 4 | from loguru import logger 5 | from redis.exceptions import LockError 6 | 7 | from app import crud, models, schemas 8 | from app.db.session import redis_session 9 | from app.scheduler import scheduler 10 | from app.util import sample 11 | 12 | def task_scheduler_job_name(task_id: UUID): 13 | return f"task_scheduler_job_{task_id}" 14 | 15 | 16 | # 标注任务定时数据处理 17 | async def label_task_scheduler_job(task_id: UUID): 18 | try: 19 | async with redis_session.lock( 20 | f"label_task_scheduler_job_{task_id}", blocking=False 21 | ): 22 | logger.info(f"Start schedulel job for label task {task_id}") 23 | 24 | task = await crud.label_task.query(task_id=task_id).first_or_none() 25 | if task is None: 26 | logger.error(f"Label task {task_id} not found") 27 | scheduler.remove_job(job_id=task_scheduler_job_name(task_id)) 28 | return 29 | 30 | # 清理过期的数据 31 | records = await crud.record.query( 32 | task_id=task_id, 33 | create_time_lt=int(time.time()) - task.expire_time, 34 | is_submit=False, 35 | ).to_list() 36 | for record in records: 37 | data = await crud.data.query(data_id=record.data_id).first_or_none() 38 | if data is None: 39 | logger.error(f"Data {record.data_id} not found") 40 | continue 41 | await crud.record.remove(_id=record.id) 42 | await crud.data.update( 43 | db_obj=data, 44 | obj_in=models.data.DataUpdate( 45 | status=schemas.data.DataStatus.PENDING 46 | ), 47 | ) 48 | 49 | logger.info(f"Done schedulel job for label task {task_id}") 50 | 51 | except LockError: 52 | logger.info(f"Skip schedulel job for label task {task_id}") 53 | 54 | -------------------------------------------------------------------------------- /backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | data, 3 | evaluation, 4 | file, 5 | message, 6 | operator, 7 | record, 8 | task, 9 | team, 10 | tool, 11 | user, 12 | ) 13 | -------------------------------------------------------------------------------- /backend/app/schemas/evaluation.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class QuestionnaireEvaluation(BaseModel): 5 | is_invalid_questionnaire: bool = Field(description="是否为无效问卷", default=False) 6 | 7 | 8 | class LabelEvaluation(BaseModel): 9 | # 针对单条消息的评价 10 | message_evaluation: dict | None = Field( 11 | description="针对单条消息的评价", default=None 12 | ) 13 | 14 | # 针对整个对话的评价 15 | conversation_evaluation: dict | None = Field( 16 | description="针对整个对话的评价", default=None 17 | ) 18 | 19 | # 针对整个问卷的评价 20 | questionnaire_evaluation: QuestionnaireEvaluation | None = Field( 21 | description="针对整个问卷的评价", default=None 22 | ) 23 | 24 | 25 | class AuditEvaluation(BaseModel): 26 | # 针对本条数据的评价 27 | data_evaluation: list[dict] | None = Field( 28 | description="针对本条数据的评价", default=None 29 | ) 30 | 31 | 32 | class SingleEvaluation(LabelEvaluation): 33 | data_evaluation: dict | None = Field(description="针对本条数据的评价", default=None) 34 | 35 | 36 | class Evaluation(LabelEvaluation, AuditEvaluation): 37 | 38 | def to_label_evaluation(self) -> LabelEvaluation: 39 | return LabelEvaluation( 40 | message_evaluation=self.message_evaluation, 41 | conversation_evaluation=self.conversation_evaluation, 42 | questionnaire_evaluation=self.questionnaire_evaluation, 43 | ) 44 | -------------------------------------------------------------------------------- /backend/app/schemas/file.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class ReqCreateFileUploadUrl(BaseModel): 7 | """ 8 | 创建文件上传链接请求 9 | """ 10 | 11 | type: Literal[ 12 | "image/png", 13 | "image/jpeg", 14 | "image/gif", 15 | "video/mp4", 16 | "video/quicktime", 17 | "audio/mpeg", 18 | ] = Field(..., description="文件类型") 19 | content_length: int = Field(..., description="文件大小(50M以内)", gt=0, le=52428800) 20 | suffix: Literal[ 21 | "png", 22 | "jpg", 23 | "jpeg", 24 | "gif", 25 | "mp4", 26 | "mov", 27 | "mp3", 28 | ] = Field(..., description="文件后缀") 29 | 30 | 31 | class RespCreateFileUploadUrl(BaseModel): 32 | """ 33 | 创建文件上传链接响应 34 | """ 35 | 36 | put_url: str = Field(..., description="上传链接") 37 | get_url: str = Field(..., description="下载链接") 38 | -------------------------------------------------------------------------------- /backend/app/schemas/message.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from typing import Literal 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class MessageBase(BaseModel): 8 | message_id: UUID = Field(description="消息ID") 9 | parent_id: UUID | None = Field(description="父消息ID", default=None) 10 | message_type: Literal["send", "receive"] = Field(description="消息类型") 11 | content: str = Field(description="消息内容") 12 | 13 | 14 | class Message(MessageBase): 15 | user_id: str = Field(description="用户ID", default="") 16 | -------------------------------------------------------------------------------- /backend/app/schemas/operator/__init__.py: -------------------------------------------------------------------------------- 1 | from . import task 2 | from . import stats -------------------------------------------------------------------------------- /backend/app/schemas/record.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | from app.schemas.data import DoDataBase 6 | from app.schemas.evaluation import Evaluation 7 | 8 | 9 | class RecordStatus(str, Enum): 10 | """ 11 | 数据状态 12 | """ 13 | 14 | # 加工中 15 | PROCESSING = "processing" 16 | # 已完成 17 | COMPLETED = "completed" 18 | # 已废弃 19 | DISCARDED = "discarded" 20 | 21 | 22 | class RecordFullStatus(str, Enum): 23 | """ 24 | 数据状态 25 | """ 26 | 27 | # 加工中 28 | PROCESSING = "processing" 29 | # 已完成 30 | COMPLETED = "completed" 31 | # 已废弃 32 | DISCARDED = "discarded" 33 | # 审核通过 34 | APPROVED = "approved" 35 | # 审核未通过 36 | REJECTED = "rejected" 37 | # 无效问卷 38 | INVALID = "invalid" 39 | 40 | 41 | class ViewGroupUser(BaseModel): 42 | user_id: str = Field(description="用户id", alias="_id") 43 | completed_data_count: int = Field(description="答题数") 44 | discarded_data_count: int = Field(description="未达标题数") 45 | 46 | 47 | class DoRecord(DoDataBase): 48 | """ 49 | 数据信息 50 | """ 51 | 52 | flow_index: int = 1 53 | creator_id: str 54 | create_time: int 55 | submit_time: int 56 | evaluation: Evaluation 57 | status: RecordStatus 58 | -------------------------------------------------------------------------------- /backend/app/schemas/tool.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ToolTranslateResponse(BaseModel): 7 | text: str 8 | 9 | 10 | class ToolTranslateRequest(BaseModel): 11 | text: str 12 | source: Literal["EN", "ZH"] 13 | target: Literal["EN-GB", "EN-US", "ZH"] 14 | 15 | class ToolGoogleTranslateResponse(BaseModel): 16 | text: str 17 | source: Literal["ar", "cs", "hu", "sr", "ru", "ko", "vi", "th", "de", "fr", "ja", "zh", "en"] 18 | target: Literal["ar", "cs", "hu", "sr", "ru", "ko", "vi", "th", "de", "fr", "ja", "zh", "en"] 19 | -------------------------------------------------------------------------------- /backend/app/schemas/user.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from uuid import UUID 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | from .team import TeamMemberRole, TeamMember 7 | 8 | 9 | class UserType(str, Enum): 10 | """ 11 | 用户类型 12 | """ 13 | 14 | # 管理员 15 | SUPER_ADMIN = "super_admin" 16 | # 运营 17 | ADMIN = "admin" 18 | # 普通用户 19 | USER = "user" 20 | 21 | 22 | class DoUserBase(BaseModel): 23 | user_id: str = Field(description="用户ID") 24 | 25 | 26 | class DoUserWithUsername(DoUserBase): 27 | username: str = Field(description="用户名") 28 | 29 | 30 | class DoUser(DoUserBase): 31 | role: UserType = Field(description="用户角色") 32 | 33 | 34 | class EditUserInfo(BaseModel): 35 | user_id: str | None = Field(description="用户ID", default=None) 36 | name: str | None = Field(description="用户名称", default=None) 37 | role: UserType = Field(description="用户角色") 38 | 39 | 40 | class UserInfo(BaseModel): 41 | user_id: str = Field(description="用户ID") 42 | role: UserType = Field(description="用户角色") 43 | 44 | # 用户名称 45 | name: str = Field(description="用户名称") 46 | 47 | 48 | class ListUserResp(BaseModel): 49 | list: list[UserInfo] 50 | total: int 51 | 52 | 53 | class UserTeamInfo(BaseModel): 54 | user_id: str = Field(description="用户ID") 55 | role: TeamMemberRole = Field(description="用户在团队中的角色") 56 | 57 | # 用户名称 58 | team_id: UUID = Field(description="团队 id") 59 | 60 | 61 | class ListUserTeamInfoResp(BaseModel): 62 | list: list[UserTeamInfo] 63 | 64 | class UserLoginRequest(BaseModel): 65 | username: str = Field(..., description="用户名") 66 | password: str = Field(..., description="密码") 67 | 68 | class RespMe(DoUser): 69 | name: str = Field(..., description="用户名") 70 | teams: list[TeamMember] | None = Field(description="teams that user joined", default=None) 71 | 72 | 73 | class ListUserTaskResp(BaseModel): 74 | list: list[DoUserWithUsername] 75 | total: int -------------------------------------------------------------------------------- /backend/app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/app/tests/__init__.py -------------------------------------------------------------------------------- /backend/app/util/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | 5 | def sample(ratio, n): 6 | """ 7 | ratio in [0, 100] 8 | n >= 0 9 | """ 10 | if 0 >= n: 11 | return [] 12 | 13 | return [i for i in range(n) if ratio >= random.randint(1, 100)] 14 | -------------------------------------------------------------------------------- /backend/app/worker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import signal 4 | import sys 5 | 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | from app.db.init_db import close_db, init_db 9 | from app.scheduler.init_scheduler import scheduler 10 | 11 | 12 | def shutdown(): 13 | print("shutdown") 14 | # 关闭定时任务 15 | scheduler.shutdown(wait=True) 16 | # 关闭数据库 17 | loop.run_until_complete(close_db()) 18 | loop.stop() 19 | 20 | 21 | async def main(): 22 | # 初始化数据库 23 | await init_db() 24 | # 初始化定时任务 25 | scheduler.start() 26 | 27 | 28 | if __name__ == "__main__": 29 | loop = asyncio.get_event_loop() 30 | for sig in (signal.SIGINT, signal.SIGTERM): 31 | loop.add_signal_handler(sig, shutdown) 32 | loop.run_until_complete(main()) 33 | loop.run_forever() 34 | loop.close() 35 | -------------------------------------------------------------------------------- /backend/migrations/2023_0615_1112_mig_team_member.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | sys.path.append('.') 5 | 6 | import asyncio 7 | 8 | from beanie import Document, init_beanie 9 | 10 | from app.core.config import settings 11 | from app.db.session import mongo_session 12 | from app.schemas.team import TeamMember, TeamMemberRole 13 | 14 | 15 | """ 16 | 升级 team 中的用户信息, users_ids: lsit[str] ====> users: list[TeamMember] | None 17 | 升级后,不删除 user_ids 字段 18 | 升级脚本具有幂等性质,可以多次运行 19 | """ 20 | 21 | 22 | class Team(Document): 23 | # 用户 id 列 24 | users: list[TeamMember] | None 25 | 26 | # 之前的用户列 27 | user_ids: list[str] | None 28 | 29 | 30 | async def init_db(): 31 | await init_beanie( 32 | database=mongo_session[settings.MongoDB_DB_NAME], 33 | document_models=[Team], # type: ignore 34 | ) 35 | 36 | teams = await Team.find_all().to_list() 37 | for team in teams: 38 | if not team.users: 39 | team.users = [] 40 | if team.user_ids: 41 | for v in team.user_ids: 42 | team.users.append(TeamMember(user_id=v, role=TeamMemberRole.USER)) 43 | await team.save() #type: ignore 44 | 45 | 46 | async def close_db(): 47 | mongo_session.close() 48 | 49 | 50 | async def mig(): 51 | await init_db() 52 | await close_db() 53 | 54 | if __name__ == '__main__': 55 | asyncio.run(mig()) 56 | -------------------------------------------------------------------------------- /backend/pdm.toml: -------------------------------------------------------------------------------- 1 | [pypi] 2 | url = "https://mirrors.aliyun.com/pypi/simple/" 3 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pdm] 2 | [tool.pdm.dev-dependencies] 3 | dev = [ 4 | "black>=23.3.0", 5 | "flake8>=6.0.0", 6 | "isort>=5.12.0", 7 | "mypy>=1.2.0", 8 | "types-redis>=4.5.5.2", 9 | ] 10 | 11 | [project] 12 | name = "svc" 13 | version = "0.1.0" 14 | description = "" 15 | authors = [ 16 | {name = "Suven", email = "suchenlin@pjlab.org.cn"}, 17 | ] 18 | dependencies = [ 19 | "fastapi[all]>=0.95.1", 20 | "uvicorn[standard]>=0.21.1", 21 | "python-jose[cryptography]>=3.3.0", 22 | "passlib[bcrypt]>=1.7.4", 23 | "motor>=3.1.2", 24 | "beanie>=1.18.0", 25 | "cryptography==43.0.1", 26 | "gunicorn>=20.1.0", 27 | "apscheduler>=3.10.1", 28 | "loguru>=0.7.0", 29 | "redis[hiredis]>=4.5.5", 30 | "pytz>=2023.3", 31 | "openpyxl>=3.1.2", 32 | "requests>=2.31.0", 33 | "oss2>=2.18.0", 34 | "minio>=7.2.7", 35 | "python-magic>=0.4.27", 36 | "pydantic-settings>=2.7.0", 37 | "sentry-sdk[fastapi]>=2.19.2", 38 | ] 39 | requires-python = ">=3.10" 40 | readme = "README.md" 41 | license = {text = "MIT"} 42 | 43 | [tool.isort] 44 | multi_line_output = 3 45 | include_trailing_comma = true 46 | force_grid_wrap = 0 47 | line_length = 88 48 | 49 | [build-system] 50 | requires = ["pdm-backend"] 51 | build-backend = "pdm.backend" 52 | 53 | [tool.pdm.scripts] 54 | 55 | app.cmd = "python app/main.py" 56 | app.env_file = ".env" -------------------------------------------------------------------------------- /backend/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | if [ -f /app/app/main.py ]; then 5 | DEFAULT_MODULE_NAME=app.main 6 | elif [ -f /app/main.py ]; then 7 | DEFAULT_MODULE_NAME=main 8 | fi 9 | MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME} 10 | VARIABLE_NAME=${VARIABLE_NAME:-app} 11 | export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"} 12 | echo "Starting Gunicorn with $APP_MODULE" 13 | 14 | if [ -f /app/gunicorn_conf.py ]; then 15 | DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py 16 | elif [ -f /app/app/gunicorn_conf.py ]; then 17 | DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py 18 | else 19 | DEFAULT_GUNICORN_CONF=/gunicorn_conf.py 20 | fi 21 | export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF} 22 | export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"} 23 | 24 | # If there's a prestart.sh script in the /app directory or other path specified, run it before starting 25 | PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh} 26 | echo "Checking for script in $PRE_START_PATH" 27 | if [ -f $PRE_START_PATH ] ; then 28 | echo "Running script $PRE_START_PATH" 29 | . "$PRE_START_PATH" 30 | else 31 | echo "There is no script $PRE_START_PATH" 32 | fi 33 | 34 | # Start Gunicorn 35 | exec python -m gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE" -------------------------------------------------------------------------------- /backend/scripts/worker.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | exec python app/worker.py -------------------------------------------------------------------------------- /backend/tests/test.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/backend/tests/test.py -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | 5 | redis: 6 | image: redis:5.0 7 | restart: always 8 | ports: 9 | - "16280:6379" 10 | volumes: 11 | - redis_data:/data 12 | 13 | mongo: 14 | image: mongo:4.2 15 | restart: always 16 | ports: 17 | - "16019:27017" 18 | environment: 19 | MONGO_INITDB_ROOT_USERNAME: root 20 | MONGO_INITDB_ROOT_PASSWORD: mypassword 21 | volumes: 22 | - mongo_data:/data/db 23 | 24 | minio: 25 | image: docker.io/bitnami/minio:2022 26 | ports: 27 | - '9000:9000' 28 | - '9001:9001' 29 | environment: 30 | - MINIO_ROOT_USER=user 31 | - MINIO_ROOT_PASSWORD=password 32 | - MINIO_DEFAULT_BUCKETS=label-llm-test 33 | volumes: 34 | - minio_data:/data 35 | 36 | backend: 37 | build: ./backend 38 | ports: 39 | - '16666:8080' 40 | 41 | frontend: 42 | build: ./frontend 43 | ports: 44 | - '8086:80' 45 | depends_on: 46 | - backend 47 | 48 | volumes: 49 | redis_data: 50 | mongo_data: 51 | minio_data: -------------------------------------------------------------------------------- /frontend/.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "body-max-line-length": [ 7 | 0, 8 | "always", 9 | "Infinity" 10 | ], 11 | "type-enum": [ 12 | 2, 13 | "always", 14 | [ 15 | "feat", 16 | "update", 17 | "fix", 18 | "refactor", 19 | "optimize", 20 | "style", 21 | "docs", 22 | "chore", 23 | "test", 24 | "perf", 25 | "revert" 26 | ] 27 | ], 28 | "header-max-length": [0, "always", 100], 29 | "header-case": [ 30 | 0, 31 | "never" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.js] 14 | quote_type = single 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | .next 5 | build 6 | mock 7 | scripts/*.js 8 | public 9 | __tests__ 10 | storybook-static 11 | cy-example 12 | **/*.stories.ts 13 | **/*.stories.tsx 14 | **/*.test.ts 15 | **/*.test.tsx 16 | es 17 | .eslintrc.js 18 | rollup.config.ts 19 | tsconfig.json 20 | tailwind.config.js 21 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier', 8 | 'plugin:prettier/recommended', 9 | ], 10 | plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], 11 | globals: { 12 | JSX: true, 13 | React: true, 14 | NodeJS: true, 15 | }, 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | ecmaVersion: 2022, 19 | sourceType: 'module', 20 | project: ['tsconfig.json'], 21 | }, 22 | rules: { 23 | 'prettier/prettier': 'error', 24 | 'no-unused-vars': 'off', 25 | 'import/named': 'off', 26 | 'react/display-name': 'off', 27 | 'react-hooks/exhaustive-deps': 'warn', 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | '@typescript-eslint/no-namespace': 0, 30 | '@typescript-eslint/no-explicit-any': 'warn', 31 | '@typescript-eslint/ban-ts-comment': 'warn', 32 | '@typescript-eslint/no-unsafe-member-access': 'off', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "$(dirname -- "$0")/_/husky.sh" 4 | 5 | #"$(pwd)/frontend/node_modules/.bin/lint-staged" --cwd ./frontend 6 | 7 | cd frontend && npx --no -- lint-staged 8 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | arch=x64 2 | platform=linux 3 | engine-strict=true 4 | registry=https://registry.npmmirror.com/ 5 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | .husky/* 3 | package.json 4 | dist 5 | build 6 | .env.* 7 | .dockerignore 8 | nginx.conf 9 | .DS_Store 10 | public/* 11 | .eslintignore 12 | .commitlintrc 13 | *.yaml 14 | *.bak 15 | *.png 16 | *.toml 17 | docker 18 | .editorconfig 19 | Dockerfile* 20 | .gitignore 21 | .prettierignore 22 | LICENSE 23 | .eslintcache 24 | *.lock 25 | yarn-error.log 26 | .history 27 | .history/* 28 | *.txt 29 | *.ico 30 | .gitkeep 31 | *.jpeg 32 | *.jpg 33 | /coverage 34 | package-lock.json 35 | tsconfig.json 36 | .gitmodules 37 | refresh_submodule.sh 38 | .idea 39 | *.gif 40 | *.py 41 | *.pyc 42 | .npmrc 43 | .yarnrc 44 | .env 45 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | proseWrap: 'never', 4 | endOfLine: 'lf', 5 | tabWidth: 2, 6 | printWidth: 120, 7 | singleQuote: true, 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const prettierConfig = require('./.prettierrc'); 2 | 3 | module.exports = { 4 | plugins: ['stylelint-prettier'], 5 | rules: { 6 | 'at-rule-no-unknown': null, 7 | 'prettier/prettier': [true, prettierConfig], 8 | 'selector-class-pattern': null, 9 | 'no-descending-specificity': null, 10 | 'declaration-block-no-redundant-longhand-properties': null, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.20.4-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | ARG GL_TOKEN 6 | ARG VITE_APP_VERSION 7 | ARG SENTRY_AUTH_TOKEN_WEB 8 | 9 | # install pnpm 10 | RUN npm install -g pnpm 11 | 12 | COPY . . 13 | 14 | # install dependencies 15 | RUN pnpm install 16 | 17 | # build app 18 | RUN npm run build:js 19 | 20 | FROM nginx:1.21-alpine 21 | COPY --from=builder /app/dist /usr/share/nginx/html 22 | COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf 23 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 大语言模型平台-Web 端 2 | 3 | ## 开始 4 | 5 | ### 安装依赖 6 | 7 | ```bash 8 | npm install 9 | # pnpm install 10 | ``` 11 | 12 | ### 启动菜单 13 | 14 | ```bash 15 | npm start 16 | # pnpm start 17 | 18 | ? 请选择要执行的命令 (Use arrow keys) 19 | ❯ 启动登录页(login) 20 | 启动供应商端(supplier) 21 | 启动运营端(operator) 22 | 打包供应商端(supplier) 23 | 打包运营端(operator) 24 | 全部打包 25 | ``` 26 | 27 | ### 启动供应商端(supplier) 28 | 29 | ```bash 30 | npm run start:supplier 31 | # pnpm start:supplier 32 | ``` 33 | 34 | ### 启动运营端(operator) 35 | 36 | ```bash 37 | npm run start:operator 38 | # pnpm start:operator 39 | ``` 40 | 41 | ### 构建 42 | 43 | ```bash 44 | npm run build 45 | # pnpm build 46 | ``` 47 | 48 | ### 分析 49 | 50 | ```bash 51 | npm run analyze 52 | # pnpm analyze 53 | ``` 54 | 55 | ## 配置和约定 56 | 57 | ### 约定 58 | 59 | - 仅 `app` 中使用到的组件放在对应 `app` 的 `components` 目录下; 60 | - `app` 和 `app` 之间的代码不要相互引用,存在这种情况时,将代码提升到 frontend 下的其他目录如 61 | - `frontend/components` 62 | - `frontend/layouts` 63 | - `frontend/utils` 64 | - ... 65 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 23 | LabelU-LLM 24 | 25 | 26 | 27 | 28 |
29 | 30 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/mock/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock'; 2 | 3 | export default [ 4 | { 5 | url: '/api/v1/auth/me', 6 | method: 'post', 7 | timeout: 100, 8 | response: () => { 9 | return { 10 | id: '1', 11 | name: 'lisi', 12 | }; 13 | }, 14 | }, 15 | { 16 | url: '/api/v1/auth/token', 17 | method: 'post', 18 | timeout: 100, 19 | response: () => { 20 | return { 21 | id: '1', 22 | name: 'lisi', 23 | }; 24 | }, 25 | }, 26 | { 27 | url: '/api/v1/logout/all', 28 | method: 'post', 29 | timeout: 100, 30 | response: () => { 31 | return null; 32 | }, 33 | }, 34 | { 35 | url: '/api/v1/auth/fakeRegister', 36 | method: 'post', 37 | timeout: 100, 38 | response: () => { 39 | return { 40 | id: '1', 41 | name: 'lisi', 42 | }; 43 | }, 44 | }, 45 | ] as MockMethod[]; 46 | -------------------------------------------------------------------------------- /frontend/mock/task/taskList.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from 'vite-plugin-mock'; 2 | 3 | export default [ 4 | { 5 | url: '/api/v1/operator/task/list', 6 | method: 'get', 7 | timeout: 200, 8 | response: () => { 9 | return new Array(100).fill(0).map((_, index) => { 10 | return { 11 | id: index, 12 | name: '很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称', 13 | status: ['unready', 'processing'][index % 2], 14 | progress: 20, 15 | created_by: '我是创建人', 16 | created_at: '2021-08-01 12:00:00', 17 | updated_at: '2021-08-01 12:00:00', 18 | }; 19 | }); 20 | }, 21 | }, 22 | { 23 | url: '/api/v1/operator/task/list/:id', 24 | method: 'get', 25 | timeout: 200, 26 | response: () => { 27 | return { 28 | id: 3, 29 | name: '很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称很长的名称', 30 | status: 'processing', 31 | progress: 20, 32 | description: '我是描述', 33 | created_by: '我是创建人', 34 | created_at: '2021-08-01 12:00:00', 35 | updated_at: '2021-08-01 12:00:00', 36 | }; 37 | }, 38 | }, 39 | ] as MockMethod[]; 40 | -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | client_body_buffer_size 50m; 6 | client_max_body_size 100m; 7 | charset utf-8; 8 | 9 | gzip on; 10 | gzip_buffers 16 8k; 11 | gzip_comp_level 2; 12 | gzip_disable "msie6"; 13 | gzip_http_version 1.0; 14 | gzip_min_length 1k; 15 | gzip_proxied expired no-cache no-store private auth; 16 | gzip_types text/plain application/json text/css text/javascript application/javascript; 17 | gzip_vary on; 18 | 19 | add_header X-Frame-Options "SAMEORIGIN"; 20 | 21 | location ~* /favicon.(ico|svg)$ { 22 | root /usr/share/nginx/html; 23 | } 24 | 25 | location ~* /*.(js)$ { 26 | root /usr/share/nginx/html; 27 | } 28 | 29 | location / { 30 | root /usr/share/nginx/html/apps/login; 31 | index index.html; 32 | try_files $uri $uri/ /index.html; 33 | expires off; 34 | add_header Cache-Control "no-cache"; 35 | add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; 36 | } 37 | 38 | location /supplier { 39 | expires off; 40 | alias /usr/share/nginx/html/apps/supplier; 41 | try_files /index.html =404; 42 | add_header Cache-Control "no-cache"; 43 | } 44 | 45 | location /operator { 46 | expires off; 47 | alias /usr/share/nginx/html/apps/operator; 48 | try_files /index.html =404; 49 | add_header Cache-Control "no-cache"; 50 | } 51 | 52 | location /assets { 53 | alias /usr/share/nginx/html/assets; 54 | } 55 | 56 | # location ~* \.(?:css|js|png|jpg|eot|svg|ttf|woff|woff2|map)$ { 57 | # expires 1y; 58 | # access_log off; 59 | # add_header Cache-Control "public"; 60 | # } 61 | 62 | location /api { 63 | proxy_pass http://backend:8080; 64 | proxy_http_version 1.1; 65 | proxy_set_header Upgrade $http_upgrade; 66 | proxy_set_header Connection 'upgrade'; 67 | proxy_set_header Host $host; 68 | proxy_cache_bypass $http_upgrade; 69 | } 70 | 71 | location /docs { 72 | proxy_pass http://backend:8080; 73 | proxy_http_version 1.1; 74 | proxy_set_header Upgrade $http_upgrade; 75 | proxy_set_header Connection 'upgrade'; 76 | proxy_set_header Host $host; 77 | proxy_cache_bypass $http_upgrade; 78 | } 79 | 80 | # CSS, Javascript and other static files 81 | location /robots.txt { 82 | add_header Content-Type text/plain; 83 | return 200 "User-agent: *\nDisallow: /\n"; 84 | access_log off; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import terser from '@rollup/plugin-terser'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | 6 | const plugins = [ 7 | typescript({ 8 | compilerOptions: { lib: ['es5', 'es6'], target: 'es5' }, 9 | tsconfig: './tsconfig.util.json', 10 | }), 11 | nodeResolve(), 12 | commonjs(), 13 | terser(), 14 | ]; 15 | 16 | export default [ 17 | { 18 | input: './scripts/bootstrap.ts', 19 | output: { 20 | file: './scripts/bootstrap.js', 21 | format: 'cjs', 22 | }, 23 | plugins, 24 | }, 25 | { 26 | input: './scripts/generate_css_variables_from_antd_theme_token.ts', 27 | output: { 28 | file: './scripts/generate_css_variables_from_antd_theme_token.js', 29 | format: 'cjs', 30 | }, 31 | plugins, 32 | }, 33 | ] as any; 34 | -------------------------------------------------------------------------------- /frontend/scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const minimist = require('minimist'); 3 | const shell = require('shelljs'); 4 | 5 | const rootDir = path.resolve(__dirname, '..'); 6 | 7 | async function main(args) { 8 | const [command] = args._; 9 | const appDir = args.path; 10 | 11 | process.env.APP_DIR = appDir || '*'; 12 | 13 | switch (command) { 14 | case 'build': 15 | shell.exec(`vite --config ${path.resolve(rootDir, 'vite.config.prod.ts')} build`); 16 | break; 17 | 18 | case 'dev': 19 | default: 20 | if (!appDir) { 21 | throw new Error('Missing app directory'); 22 | } 23 | 24 | // 将apps/*下的index.html移动到根目录 25 | const indexHtml = path.resolve(rootDir, appDir, 'index.html'); 26 | const rootIndexHtml = path.resolve(rootDir, 'index.html'); 27 | const basename = path.basename(appDir); 28 | 29 | shell.cp(indexHtml, rootIndexHtml); 30 | 31 | console.log('starting app =>', appDir); 32 | console.log('index.html 文件已复制'); 33 | 34 | shell.exec(`vite --config ${path.resolve(rootDir, 'vite.config.dev.ts')} --open ${basename}`); 35 | break; 36 | } 37 | } 38 | 39 | main(minimist(process.argv.slice(2))); 40 | -------------------------------------------------------------------------------- /frontend/scripts/generate_css_variables_from_antd_theme_token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: !0 }); 3 | var e, 4 | t = require('tslib'), 5 | r = t.__importDefault(require('fs')), 6 | n = t.__importDefault(require('path')), 7 | a = t.__importDefault(require('lodash')), 8 | i = require('antd'), 9 | o = t.__importDefault(require('prettier')), 10 | l = n.default.join(__dirname, '../src/styles/global-variables.css'), 11 | s = n.default.join(__dirname, '../src/styles/theme.json'), 12 | u = (0, i.theme.defaultAlgorithm)(i.theme.defaultSeed); 13 | try { 14 | var c = t.__assign(t.__assign({}, u), require(s).token), 15 | d = a.default 16 | .chain(c) 17 | .keys() 18 | .map(function (e) { 19 | var t = e 20 | .replace(/([A-Z])+/g, function (e) { 21 | return '-'.concat(e); 22 | }) 23 | .toLowerCase(); 24 | if ( 25 | t.includes('size') || 26 | t.includes('border-radius') || 27 | t.includes('control-height') || 28 | t.includes('line-width-bold') 29 | ) 30 | return '--'.concat(t, ': ').concat(c[e], 'px;'); 31 | var r = c[e]; 32 | return ( 33 | 'number' == typeof r && r.toString().length > 5 && (r = parseFloat(r.toFixed(2))), 34 | '--'.concat(t, ': ').concat(r, ';') 35 | ); 36 | }) 37 | .value(); 38 | (e = 39 | '\n /**\n * 此文件由scripts/generate_css_variables_from_antd_theme_token.ts脚本生成\n * 请勿直接修改此文件\n * */\n :root {\n '.concat( 40 | d.join('\n'), 41 | '\n }\n ', 42 | )), 43 | r.default.unlinkSync(l); 44 | } catch (e) { 45 | } finally { 46 | e && 47 | r.default.writeFile(l, o.default.format(e, { parser: 'css' }), 'utf-8', function () { 48 | console.log('🎉 '.concat(l, '已生成')); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/scripts/generate_css_variables_from_antd_theme_token.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import _ from 'lodash'; 5 | import { theme } from 'antd'; 6 | import prettier from 'prettier'; 7 | 8 | let codeTemplate; 9 | const targetPath = path.join(__dirname, '../src/styles/global-variables.css'); 10 | const tokenPath = path.join(__dirname, '../src/styles/theme.json'); 11 | const { defaultAlgorithm, defaultSeed } = theme; 12 | 13 | const mapToken = defaultAlgorithm(defaultSeed); 14 | 15 | try { 16 | const newTheme = { 17 | ...mapToken, 18 | ...require(tokenPath).token, 19 | }; 20 | 21 | const result = _.chain(newTheme) 22 | .keys() 23 | .map((key: string) => { 24 | const newKey = key 25 | .replace(/([A-Z])+/g, (match) => { 26 | return `-${match}`; 27 | }) 28 | .toLowerCase(); 29 | if ( 30 | newKey.includes('size') || 31 | newKey.includes('border-radius') || 32 | newKey.includes('control-height') || 33 | newKey.includes('line-width-bold') 34 | ) { 35 | return `--${newKey}: ${newTheme[key]}px;`; 36 | } 37 | 38 | let value = newTheme[key]; 39 | 40 | if (typeof value === 'number' && value.toString().length > 5) { 41 | value = parseFloat(value.toFixed(2)); 42 | } 43 | 44 | return `--${newKey}: ${value};`; 45 | }) 46 | .value(); 47 | 48 | codeTemplate = ` 49 | /** 50 | * 此文件由scripts/generate_css_variables_from_antd_theme_token.ts脚本生成 51 | * 请勿直接修改此文件 52 | * */ 53 | :root { 54 | ${result.join('\n')} 55 | } 56 | `; 57 | 58 | fs.unlinkSync(targetPath); 59 | } catch (err) { 60 | } finally { 61 | if (codeTemplate) { 62 | fs.writeFile( 63 | targetPath, 64 | prettier.format(codeTemplate, { 65 | parser: 'css', 66 | }), 67 | 'utf-8', 68 | () => { 69 | console.log(`🎉 ${targetPath}已生成`); 70 | }, 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/scripts/guide.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const inquirer = require('inquirer'); 3 | 4 | const options = [ 5 | { 6 | type: 'list', 7 | name: 'command', 8 | message: '请选择要执行的命令', 9 | choices: [ 10 | { 11 | key: 0, 12 | name: '启动登录页(login)', 13 | value: 'start:login', 14 | }, 15 | { 16 | key: 1, 17 | name: '启动供应商端(supplier)', 18 | value: 'start:supplier', 19 | }, 20 | { 21 | key: 2, 22 | name: '启动运营端(operator)', 23 | value: 'start:operator', 24 | }, 25 | { 26 | key: 3, 27 | name: '打包供应商端(supplier)', 28 | value: 'build:supplier', 29 | }, 30 | { 31 | key: 4, 32 | name: '打包运营端(operator)', 33 | value: 'build:operator', 34 | }, 35 | { 36 | key: 5, 37 | name: '全部打包', 38 | value: 'build', 39 | }, 40 | ], 41 | }, 42 | ]; 43 | 44 | function main() { 45 | inquirer.prompt(options).then((answers) => { 46 | shell.exec(`pnpm ${answers.command}`); 47 | }); 48 | } 49 | 50 | main(); 51 | -------------------------------------------------------------------------------- /frontend/src/api/errorCode.ts: -------------------------------------------------------------------------------- 1 | import store from 'storejs'; 2 | 3 | // 特殊 code 业务里自行处理 4 | export enum EECode { 5 | // 跳过任务 code 6 | RELEASE_ERROR = 400305, 7 | // 没有权限 8 | USER_PERMISSION_DENIED = 403002, 9 | // 翻译服务不可用 10 | TRANSLATE_SERVICE_UNAVAILABLE = 400801, 11 | } 12 | 13 | export const ECode = { 14 | 500001: '服务器错误', 15 | 403002: '用户权限不足', 16 | 403003: '参数格式错误', 17 | 4011001: 'Token 无效', 18 | // 用户 19 | 400201: '此账号不存在,请确定账号已登录过平台', 20 | 400202: '此账号已是运营,添加失败', 21 | 400203: '此账号已是运营,添加失败', 22 | // 任务 23 | 400301: '任务不存在', 24 | 400302: '任务已结束, 不支持修改', 25 | 400303: '任务流程不存在', 26 | 400304: '此标注任务已存在对应的审核任务,请选择其他任务', 27 | // 数据 28 | 400401: '数据不存在', 29 | 400402: '数据不属于用户', 30 | 400403: '此任务已无更多题目了,请换个任务吧', 31 | // 团队 32 | 400901: '团队不存在', 33 | 400902: '默认团队不允许移除', 34 | 400903: '用户尚未加入该团队', 35 | 400904: '用户已加入该团队', 36 | 400905: '团队成员角色为空', 37 | 400906: '团队应该至少有一个超级管理员', 38 | 400951: '链接不存在', 39 | } as Record; 40 | 41 | const ECodeEn = { 42 | 500001: 'Server error', 43 | 403002: 'Insufficient user permissions', 44 | 403003: 'Invalid parameter format', 45 | 4011001: 'Invalid Token', 46 | // User 47 | 400201: 'This account does not exist, please ensure the account has logged into the platform', 48 | 400202: 'This account is already an operator, addition failed', 49 | 400203: 'This account is already an operator, addition failed', 50 | // Task 51 | 400301: 'Task does not exist', 52 | 400302: 'An error occurred, please choose a different task', 53 | 400303: 'Task process does not exist', 54 | 400304: '此标注任务已存在对应的审核任务,请选择其他任务', 55 | // Data 56 | 400401: 'Data does not exist', 57 | 400402: 'Data does not belong to the user', 58 | 400403: 'No more questions available for this task, please choose a different task', 59 | // Team 60 | 400901: 'Team does not exist', 61 | 400902: 'The default team cannot be removed', 62 | 400903: 'User has not joined the team', 63 | 400904: 'User has already joined the team', 64 | 400905: 'Team member role is empty', 65 | 400906: 'The team should have at least one super administrator', 66 | 400951: 'Link does not exist', 67 | } as Record; 68 | 69 | export const getErrorText = (code: number) => { 70 | const lang = store('lang') || 'zh-CN'; 71 | return lang === 'zh-CN' ? ECode[code] : ECodeEn[code]; 72 | }; 73 | -------------------------------------------------------------------------------- /frontend/src/api/request.tsx: -------------------------------------------------------------------------------- 1 | import { notification } from 'antd'; 2 | import _ from 'lodash'; 3 | import type { AxiosError, AxiosResponse } from 'axios'; 4 | import axios from 'axios'; 5 | 6 | import { goLogin } from '@/utils/sso'; 7 | import { message } from '@/components/StaticAnt'; 8 | 9 | import { EECode, ECode, getErrorText } from './errorCode'; 10 | 11 | /** 12 | * @param response 13 | * @returns 14 | */ 15 | export function successHandler(response: AxiosResponse) { 16 | return response.data; 17 | } 18 | 19 | function errorHandler(error: AxiosError) { 20 | const errMsgFromServer = _.get(error, 'response.data.detail.message'); 21 | const errCode = _.get(error, 'response.data.detail.code'); 22 | 23 | // 特殊错误 code 不需要全局报错 需要在业务中自行处理 24 | if (Object.values(EECode).includes(errCode as any)) { 25 | return Promise.reject(_.get(error, 'response.data.detail')); 26 | } 27 | 28 | const errorText = (errCode && getErrorText(errCode)) || errMsgFromServer; 29 | if (errorText) { 30 | message.error(errorText); 31 | } 32 | 33 | return Promise.reject(error); 34 | } 35 | 36 | const authorizationBearerFailed = (error: any) => { 37 | // 401一秒后跳转到登录页 38 | if (error?.response?.status === 401 && !import.meta.env.DEV) { 39 | goLogin(); 40 | } 41 | // 特殊状态码 没有具体code 42 | if (error?.response?.status === 422) { 43 | message.error('数据格式错误'); 44 | } 45 | 46 | return Promise.reject(error); 47 | }; 48 | 49 | const requestConfig = { 50 | timeout: 2 * 60 * 1000, 51 | baseURL: '/api', 52 | }; 53 | 54 | const request = axios.create(requestConfig); 55 | export const plainRequest = axios.create(requestConfig); 56 | 57 | request.interceptors.response.use(successHandler, errorHandler); 58 | request.interceptors.response.use(undefined, authorizationBearerFailed); 59 | plainRequest.interceptors.response.use(undefined, errorHandler); 60 | plainRequest.interceptors.response.use(undefined, authorizationBearerFailed); 61 | 62 | export default request; 63 | -------------------------------------------------------------------------------- /frontend/src/api/team.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | /** 4 | * 获取团队成员列表 5 | * */ 6 | 7 | export enum ETeamAccess { 8 | admin = 'admin', // 管理员 9 | user = 'user', // 普通用户 10 | } 11 | export interface ITeamMemberParams { 12 | team_id: string; // 团队id 13 | name?: string; // 用户名 14 | page?: number; // 页码 15 | page_size?: number; // 每页数量 16 | role?: ETeamAccess; // 用户角色 17 | } 18 | export interface ITeamMember { 19 | user_id: string; // 用户id 20 | name: string; // 用户名 21 | role: ETeamAccess; // 用户角色 22 | } 23 | 24 | export const getTeamMemberList = (params: ITeamMemberParams): Promise<{ list: ITeamMember[]; total: number }> => { 25 | return request.post('/v1/team/member/list', params); 26 | }; 27 | 28 | /** 29 | * 删除团队成员 30 | * */ 31 | interface IDeleteTeamMemberParams { 32 | team_id: string; // 团队id 33 | user_id: string; // 用户id 34 | } 35 | export const deleteTeamMember = (params: IDeleteTeamMemberParams) => { 36 | return request.post('/v1/team/member/remove', params); 37 | }; 38 | 39 | /** 40 | * 更新团队成员 41 | * */ 42 | export interface IUpdateTeamMemberParams { 43 | team_id: string; // 团队id 44 | user_info: { 45 | user_id: string; // 用户id 46 | name: string; // 用户名 47 | role: ETeamAccess; // 用户角色 48 | }; 49 | } 50 | export const updateTeamMember = (params: IUpdateTeamMemberParams) => { 51 | return request.post('/v1/team/member/edit', params); 52 | }; 53 | 54 | /** 55 | * 创捷邀请链接 56 | * */ 57 | interface ICreateInviteLinkRes { 58 | expire_time: string; // 过期时间 59 | link_id: string; // 链接id 60 | } 61 | export const createInviteLink = (params: { team_id: string }): Promise => { 62 | return request.post('/v1/team/member/invitations/create', params); 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | 3 | export enum EUserRole { 4 | admin = 'admin', // 管理员 5 | user = 'user', // 普通用户 6 | } 7 | 8 | /** 9 | * 用户注册 username password 10 | * */ 11 | export interface ICreate { 12 | username: string; 13 | password: string; 14 | password2?: string; 15 | } 16 | export const create = (params: ICreate): Promise => { 17 | return request.post('/v1/user/create', params); 18 | }; 19 | 20 | export interface IUserInfo { 21 | user_id: number; 22 | name: string; 23 | role: EUserRole; 24 | teams?: IUserInfo[]; 25 | } 26 | 27 | /** 28 | * 用户登录 29 | * */ 30 | export const login = (params: ICreate): Promise => { 31 | return request.post('/v1/user/login', params); 32 | }; 33 | 34 | /** 35 | * 退出登录 36 | */ 37 | export const logout = (): Promise => { 38 | return request.post('/v1/user/logout'); 39 | }; 40 | 41 | /** 42 | * 获取用户信息 43 | */ 44 | export const getUserInfo = async (): Promise => { 45 | return request.get('/v1/user/me'); 46 | }; 47 | 48 | /** 49 | * 获取用户列表 50 | */ 51 | interface IUserListParams { 52 | page?: number; 53 | page_size?: number; 54 | username?: string; 55 | role?: EUserRole; 56 | is_operator?: boolean; 57 | } 58 | export const getUserList = async (params: IUserListParams): Promise<{ list: IUserInfo[]; total: number }> => { 59 | return request.post('/v1/user/list', params); 60 | }; 61 | 62 | /** 63 | * 更新用户 64 | */ 65 | export const updateUser = (params: Pick) => { 66 | return request.post('/v1/user/edit', params); 67 | }; 68 | -------------------------------------------------------------------------------- /frontend/src/apps/login/App.tsx: -------------------------------------------------------------------------------- 1 | import AppContainer from '@/components/AppContainer'; 2 | import RouterContainer from '@/components/RouterContainer'; 3 | import { QueryProvider } from '@/constant/queryClient'; 4 | 5 | import routes from './routes'; 6 | 7 | export default function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/apps/login/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 22 | LabelU-LLM 23 | 24 | 25 | 26 | 27 |
28 | 29 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/apps/login/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | 3 | import App from './App'; 4 | import '../../initialize'; 5 | import './styles/index.css'; 6 | import 'antd/dist/reset.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render(); 9 | -------------------------------------------------------------------------------- /frontend/src/apps/login/pages/login/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/login/pages/login/bg.png -------------------------------------------------------------------------------- /frontend/src/apps/login/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/apps/login/routes.tsx: -------------------------------------------------------------------------------- 1 | import Login from './pages/login'; 2 | 3 | export default [ 4 | { 5 | path: '/', 6 | element: , 7 | // 此ID可以用于在路由中获取loader中的数据 8 | id: 'root', 9 | handle: { 10 | crumb: () => { 11 | return 'LabelU-LLM'; 12 | }, 13 | }, 14 | }, 15 | { 16 | path: '/login', 17 | element: , 18 | // 此ID可以用于在路由中获取loader中的数据 19 | id: 'login', 20 | handle: { 21 | crumb: () => { 22 | return 'LabelU-LLM'; 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /frontend/src/apps/login/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/App.tsx: -------------------------------------------------------------------------------- 1 | import AppContainer from '@/components/AppContainer'; 2 | import RouterContainer from '@/components/RouterContainer'; 3 | import { QueryProvider } from '@/constant/queryClient'; 4 | 5 | import routes from './routes'; 6 | import { ReactComponent as EmptyElement } from './assets/empty.svg'; 7 | 8 | const customEmpty = () => { 9 | return ( 10 |
11 | 12 |

暂无数据

13 |
14 | ); 15 | }; 16 | 17 | export default function App() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/README.md: -------------------------------------------------------------------------------- 1 | # 供应商(标注)端 2 | 3 | 使用实验室的账号体系(SSO) 4 | 5 | 附:[SSO 接入文档](https://aicarrier.feishu.cn/docx/FYdedfGzEoV73Axi2lLcQdcrnzc) 6 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-conversation@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-conversation@1x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-conversation@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-conversation@2x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-question@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-question@1x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-question@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-question@2x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-reply@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-reply@1x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/demo-reply@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/demo-reply@2x.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/diff.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/grid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/grid1.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/grid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/grid2.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/grid3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/grid3.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/grid4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/operator/assets/grid4.png -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/noAuth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/assets/upload-cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CopyTask.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { useMutation } from '@tanstack/react-query'; 4 | 5 | import { copyTask, copyAuditTask } from '@/apps/operator/services/task'; 6 | import { modal } from '@/components/StaticAnt'; 7 | 8 | interface IProps { 9 | id: string; 10 | type: 'label' | 'audit'; 11 | } 12 | 13 | function CopyTask({ id, type }: IProps) { 14 | const { mutateAsync, isPending } = useMutation({ 15 | mutationFn: type === 'label' ? copyTask : copyAuditTask, 16 | onSuccess: (data) => { 17 | // 跳转到任务详情页面 18 | if (data.is_ok) { 19 | modal.confirm({ 20 | title: '复制任务', 21 | cancelText: '知道了', 22 | okText: '前往查看', 23 | onOk: () => { 24 | window.open(`/operator/task/${data.task_id}`, '_blank'); 25 | }, 26 | content: '任务复制成功', 27 | centered: true, 28 | }); 29 | } else { 30 | modal.error({ 31 | title: '复制任务', 32 | content: `任务复制失败, 失败原因:${data?.msg}`, 33 | centered: true, 34 | }); 35 | } 36 | }, 37 | }); 38 | 39 | const handleClick = async () => { 40 | await mutateAsync({ task_id: id }); 41 | }; 42 | return ( 43 | 46 | ); 47 | } 48 | 49 | export default CopyTask; 50 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomEmpty.tsx: -------------------------------------------------------------------------------- 1 | import type { EmptyProps } from 'antd'; 2 | import { Empty } from 'antd'; 3 | 4 | import { ReactComponent as EmptyElement } from '../assets/empty.svg'; 5 | 6 | export default function CustomEmpty(props: EmptyProps) { 7 | return } {...props} />; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/QuestionEditor/Condition/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import type { FormInstance } from 'antd'; 3 | 4 | import type { QuestionItem } from '../types'; 5 | 6 | export type ConditionContextType = { 7 | currentQuestion: QuestionItem; 8 | questions: QuestionItem[]; 9 | form: FormInstance; 10 | disabled?: boolean; 11 | } | null; 12 | 13 | export const ConditionContext = createContext(null); 14 | 15 | export function useCondition() { 16 | const value = useContext(ConditionContext); 17 | 18 | if (!value) { 19 | throw new Error('useCondition must be used within a ConditionContext'); 20 | } 21 | 22 | return value; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/QuestionEditor/TagSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import { SwapOutlined } from '@ant-design/icons'; 2 | import { Tag } from 'antd'; 3 | import { useCallback, useState } from 'react'; 4 | 5 | export interface TagSwitcherProps { 6 | value?: boolean; 7 | disabled?: boolean; 8 | onChange: (value: boolean) => void; 9 | titleMapping?: Record; 10 | } 11 | 12 | export default function TagSwitcher({ 13 | value = false, 14 | disabled, 15 | onChange, 16 | titleMapping = { true: '开', false: '关' }, 17 | }: TagSwitcherProps) { 18 | const [open, setOpen] = useState(value); 19 | 20 | const handleChange = useCallback(() => { 21 | setOpen((pre) => { 22 | onChange?.(!pre); 23 | 24 | return !pre; 25 | }); 26 | }, [onChange]); 27 | 28 | return ( 29 | 30 | {titleMapping[String(open)]} 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/QuestionEditor/svgs/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/QuestionEditor/svgs/tree-switcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/QuestionEditor/types.ts: -------------------------------------------------------------------------------- 1 | import type { ConditionItem } from '@/apps/supplier/services/task'; 2 | 3 | export enum QuestionType { 4 | Enum = 'enum', 5 | Array = 'array', 6 | String = 'string', 7 | } 8 | 9 | export interface QuestionItem { 10 | conditions?: ConditionItem[]; 11 | label: string; 12 | value: string; 13 | id: string; 14 | /** enum 为单选;array为多选;string为文本描述 */ 15 | type: Lowercase; 16 | required?: boolean; 17 | /** 以下是属性分类才有的字段 */ 18 | is_default?: boolean; 19 | options?: QuestionOption[]; 20 | } 21 | 22 | export type QuestionOption = Omit; 23 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/CustomFancy/utils.ts: -------------------------------------------------------------------------------- 1 | import type { FormInstance } from 'antd'; 2 | import type { NamePath } from 'antd/es/form/interface'; 3 | import { map, omit } from 'lodash/fp'; 4 | 5 | import { gid } from '@/utils/gid'; 6 | 7 | export function wrapWithId(item: any) { 8 | return { 9 | ...item, 10 | id: item.id || gid(), 11 | }; 12 | } 13 | 14 | export const listOmitWithId = map(omit(['id'])); 15 | 16 | export const listWrapWithId = map(wrapWithId); 17 | 18 | export const duplicatedValueValidator = 19 | (path: NamePath, index: number) => 20 | ({ getFieldValue }: FormInstance) => ({ 21 | validator(unused: any, _value: string) { 22 | const values = getFieldValue(path); 23 | 24 | for (let i = 0; i < values.length; i++) { 25 | if (i === index) { 26 | continue; 27 | } 28 | 29 | if (values[i].value === _value && _value !== undefined && _value !== '') { 30 | return Promise.reject(new Error('请勿填写重复值')); 31 | } 32 | } 33 | 34 | // 编辑通用标签时不需要再重复校验 35 | if (Array.isArray(path) && path.length === 1 && path[0] === 'attributes') { 36 | return Promise.resolve(); 37 | } 38 | 39 | // 通用标签也不可重复 40 | const commonAttributes = getFieldValue('attributes'); 41 | 42 | if (!commonAttributes) { 43 | return Promise.resolve(); 44 | } 45 | 46 | for (let i = 0; i < commonAttributes.length; i++) { 47 | if (commonAttributes[i].value === _value && _value !== undefined && _value !== '') { 48 | return Promise.reject(new Error('不可与通用标签的value重复')); 49 | } 50 | } 51 | 52 | return Promise.resolve(); 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/Help.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons'; 2 | import { Tooltip } from 'antd'; 3 | import type { TooltipProps } from 'antd'; 4 | import React from 'react'; 5 | 6 | export default function Help({ 7 | children, 8 | placement, 9 | className, 10 | }: React.PropsWithChildren<{ 11 | placement?: TooltipProps['placement']; 12 | className?: string; 13 | }>) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/NoAuth.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | 4 | import { useUserInfo } from '@/apps/operator/hooks/useUserInfo'; 5 | import CustomEmpty from '@/apps/operator/components/CustomEmpty'; 6 | 7 | import empty from '../assets/noAuth.svg'; 8 | 9 | type IProps = HTMLAttributes; 10 | 11 | const NoAuth: React.FC> = () => { 12 | const { data } = useUserInfo(); 13 | return ( 14 |
15 | 19 |
您无相关页面操作权限,请联系运营开通
20 |
您的账户信息 - 用户名:{data?.name}
21 |
22 | } 23 | /> 24 | 25 | ); 26 | }; 27 | 28 | export default NoAuth; 29 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/QueryBlock.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import type { QueryFormProps } from './QueryForm'; 4 | import QueryForm from './QueryForm'; 5 | import type { QueryTableProps } from './QueryTable'; 6 | import QueryTable from './QueryTable'; 7 | import CustomEmpty from './CustomEmpty'; 8 | 9 | export interface QueryBlockProps { 10 | formProps: QueryFormProps; 11 | tableProps: QueryTableProps; 12 | emptyDescription?: string; 13 | onSearch?: QueryFormProps['onSearch']; 14 | } 15 | 16 | export default function QueryBlock({ 17 | emptyDescription, 18 | onSearch, 19 | formProps, 20 | tableProps, 21 | }: QueryBlockProps) { 22 | let content = ( 23 | pagination={{ size: 'default', showQuickJumper: true }} onSearch={onSearch} {...tableProps} /> 24 | ); 25 | 26 | if (_.isEmpty(tableProps.dataSource) && !tableProps.loading) { 27 | content = ( 28 |
29 | 30 |
31 | ); 32 | } 33 | 34 | return ( 35 | <> 36 | 37 | {content} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/QueryTable.tsx: -------------------------------------------------------------------------------- 1 | import type { TableProps } from 'antd'; 2 | import { Table } from 'antd'; 3 | import { useMemo } from 'react'; 4 | import { useSearchParams } from 'react-router-dom'; 5 | import { styled } from 'styled-components'; 6 | 7 | import type { QueryFormProps } from './QueryForm'; 8 | 9 | export interface QueryTableProps extends TableProps { 10 | onSearch?: QueryFormProps['onSearch']; 11 | } 12 | 13 | const StyledTable = styled(Table)` 14 | .ant-table-thead > tr > th { 15 | padding: 1rem 0.75rem 1rem 1.5rem; 16 | } 17 | 18 | .ant-table-footer { 19 | background-color: transparent; 20 | padding: 1rem 1rem; 21 | } 22 | `; 23 | 24 | export default function QueryTable({ onSearch, pagination, ...rest }: QueryTableProps) { 25 | const [searchParams, setSearchParams] = useSearchParams(); 26 | const newPagination = useMemo(() => { 27 | return { 28 | ...pagination, 29 | current: Number(searchParams.get('page')) || 1, 30 | pageSize: Number(searchParams.get('page_size')) || 10, 31 | }; 32 | }, [pagination, searchParams]); 33 | 34 | const handleTableChange: TableProps['onChange'] = (tablePagination, filters, sorter) => { 35 | if (tablePagination.current) { 36 | searchParams.set('page', tablePagination.current.toString()); 37 | } 38 | 39 | if (tablePagination.pageSize) { 40 | searchParams.set('page_size', tablePagination.pageSize.toString()); 41 | } 42 | 43 | // @ts-ignore 44 | if (sorter.field) { 45 | // @ts-ignore 46 | const orderMap = sorter?.column?.orderMap || {}; 47 | // @ts-ignore 48 | const order = orderMap[sorter.order] || sorter.order; 49 | 50 | if (order) { 51 | // @ts-ignore 52 | searchParams.set('sort', order ? `${sorter.field}_${order}` : sorter.field); 53 | } else { 54 | searchParams.delete('sort'); 55 | } 56 | } 57 | 58 | setSearchParams(searchParams); 59 | onSearch?.(Object.fromEntries(searchParams.entries())); 60 | }; 61 | 62 | return ; 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/components/TableSelectedTips.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { Button, Divider, Dropdown, Tooltip } from 'antd'; 4 | import clsx from 'clsx'; 5 | 6 | import IconFont from '@/components/IconFont'; 7 | 8 | interface IExportComponentProps { 9 | disabled: boolean; 10 | isLoading: boolean; 11 | onChange: (key: string) => void; 12 | } 13 | 14 | export const ExportComponent = ({ disabled, isLoading, onChange }: IExportComponentProps) => { 15 | return ( 16 | { 25 | onChange(e.key); 26 | }, 27 | }} 28 | > 29 | 22 | } 23 | form={form} 24 | modalProps={{ 25 | width: 400, 26 | centered: true, 27 | destroyOnClose: true, 28 | onCancel: () => console.log('run'), 29 | }} 30 | onFinish={async (values) => { 31 | await mutateAsync({ ...values, role: EUserRole.admin }); 32 | message.success('添加成功'); 33 | return true; 34 | }} 35 | > 36 |
37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/pages/users.team/Edit/index.tsx: -------------------------------------------------------------------------------- 1 | import { ModalForm, ProFormText } from '@ant-design/pro-components'; 2 | import { message } from 'antd'; 3 | 4 | import type { ITeam, IUpdateTeam } from '@/apps/operator/services/team'; 5 | import { updateTeam, createTeam } from '@/apps/operator/services/team'; 6 | 7 | interface IProps { 8 | info?: ITeam; 9 | isCreate?: boolean; 10 | open: boolean; 11 | onUpdate: () => void; 12 | onCancel: () => void; 13 | } 14 | 15 | // 正则校验手机号 16 | const phoneReg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/; 17 | 18 | export default (props: IProps) => { 19 | return ( 20 | { 25 | const api = props.isCreate ? createTeam : updateTeam; 26 | await api({ ...values, team_id: props.info?.team_id as string }); 27 | message.success('提交成功'); 28 | props?.onUpdate(); 29 | return true; 30 | }} 31 | initialValues={props.info} 32 | modalProps={{ 33 | centered: true, 34 | width: 500, 35 | destroyOnClose: true, 36 | onCancel: () => props.onCancel(), 37 | }} 38 | > 39 |
40 | 50 | 58 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/services/team.ts: -------------------------------------------------------------------------------- 1 | import request from '@/api/request'; 2 | import type { ETeamAccess } from '@/api/team'; 3 | 4 | /** 5 | * 创建团队 6 | * */ 7 | export interface ITeam { 8 | name: string; // 团队名称 9 | owner: string; // 团队拥有者 联系人 10 | owner_cellphone: string; // 联系人手机号 11 | team_id: string; // 团队id 12 | user_count: number; // 团队成员数量 13 | is_default_team: boolean; // 是否是默认团队 14 | } 15 | export const createTeam = (params: Omit) => { 16 | return request.post('/v1/team/create', params); 17 | }; 18 | 19 | /** 20 | * 更新团队 21 | * */ 22 | export type IUpdateTeam = Omit; 23 | export const updateTeam = (params: IUpdateTeam) => { 24 | return request.patch('/v1/team/update', params); 25 | }; 26 | 27 | /** 28 | * 获取团队列表 29 | * */ 30 | interface ITeamListParams { 31 | page?: number; // 页码 32 | page_size?: number; // 每页数量 33 | } 34 | 35 | export const getTeamList = (params: ITeamListParams): Promise<{ list: ITeam[]; total: number }> => { 36 | return request.post('/v1/team/list', params); 37 | }; 38 | 39 | /** 40 | * 获取团队详情 41 | * */ 42 | export const getTeamDetail = async (team_id: string): Promise => { 43 | return request.get(`/v1/team/get/${team_id}`); 44 | }; 45 | 46 | /** 47 | * 删除团队 48 | */ 49 | export const deleteTeam = (params: { team_id: string }) => { 50 | return request.post('/v1/team/delete', params); 51 | }; 52 | 53 | /** 54 | * 获取运营人员列表 55 | * */ 56 | export interface IOperatorItemParams { 57 | user_name?: string; // 用户名 58 | role?: ETeamAccess; // 用户角色 59 | page_size: number; // 每页数量 60 | is_operator: boolean; // 是否是运营人员 61 | page?: number; // 页码 62 | } 63 | export interface IOperatorRes { 64 | user_id: string; // 用户名 65 | role: ETeamAccess; // 用户角色 66 | name?: string; // 用户名 67 | } 68 | /** 69 | * 添加运营人员 删除运营人员 70 | * */ 71 | export const editOperator = (params: IOperatorRes) => { 72 | return request.post('/v1/user/edit', params); 73 | }; 74 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/services/types.ts: -------------------------------------------------------------------------------- 1 | export interface ListPayload { 2 | total: number; 3 | list: T[]; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --header-height: 56px; 7 | } 8 | 9 | a, 10 | a:hover { 11 | @apply text-primary; 12 | } 13 | 14 | /* 全局样式 15 | 设计要求覆盖全平台 16 | */ 17 | 18 | /* 弹窗 内容区 */ 19 | .ant-modal-content { 20 | padding-bottom: 24px !important; 21 | } 22 | 23 | .ant-table-thead .ant-table-cell { 24 | @apply !bg-fill-tertiary; 25 | } 26 | 27 | input:-webkit-autofill, 28 | input:-webkit-autofill:hover, 29 | input:-webkit-autofill:focus, 30 | input:-webkit-autofill:active { 31 | box-shadow: 0 0 0 30px white inset !important; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/utils/bfsEach.ts: -------------------------------------------------------------------------------- 1 | interface BFSOption { 2 | childrenField: ChildField; 3 | } 4 | 5 | export type TreeTypeWithChildrenField = TreeNode & 6 | Record[]>; 7 | 8 | /** 9 | * 广度优先遍历树节点 10 | * @param input 树 11 | * @param iteratee 回调函数 12 | * @param option 13 | * @returns void 14 | */ 15 | export function bfsEach( 16 | input: TreeTypeWithChildrenField[], 17 | iteratee: ( 18 | node: TreeTypeWithChildrenField, 19 | i: number, 20 | input: TreeTypeWithChildrenField[], 21 | parentPath: (string | number)[], 22 | ) => void, 23 | option: BFSOption = { childrenField: 'children' as ChildField }, 24 | path: (string | number)[] = [], 25 | ) { 26 | if (!Array.isArray(input)) { 27 | console.warn('bfsEach input must be an array'); 28 | return; 29 | } 30 | 31 | const { childrenField } = option; 32 | 33 | const leaf: TreeTypeWithChildrenField[] = []; 34 | 35 | for (let i = 0; i < input.length; i += 1) { 36 | if (Array.isArray(input[i][childrenField])) { 37 | leaf.push(...input[i][childrenField]); 38 | } 39 | 40 | iteratee(input[i], i, input, [...path, i.toString()]); 41 | 42 | if (leaf.length > 0 && i === input.length - 1) { 43 | bfsEach(leaf, iteratee, option, [...path, childrenField]); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/utils/downloadFromUrl.ts: -------------------------------------------------------------------------------- 1 | export default function downloadFromUrl(url: string, name?: string) { 2 | const link = document.createElement('a'); 3 | link.href = url; 4 | 5 | if (name) { 6 | link.setAttribute('download', name); 7 | } 8 | 9 | document.body.appendChild(link); 10 | link.click(); 11 | document.body.removeChild(link); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/utils/mapTree.ts: -------------------------------------------------------------------------------- 1 | import type { TreeTypeWithChildrenField } from './bfsEach'; 2 | 3 | export interface MapTreeIterateeArg { 4 | index: number; 5 | treeNodes: TreeNode[]; 6 | depth: number; 7 | parent: TreeNode | null; 8 | preNode: TreeNode | undefined; 9 | } 10 | 11 | export interface MapTreeOptions { 12 | depth?: number; 13 | parent?: TreeTypeWithChildrenField | null; 14 | childrenField: ChildField; 15 | } 16 | 17 | /** 18 | * 树的map遍历 19 | * @param treeNodes 20 | * @param iteratee 21 | * @returns 22 | */ 23 | export function mapTree( 24 | treeNodes: TreeTypeWithChildrenField[] = [], 25 | iteratee: ( 26 | node: TreeTypeWithChildrenField, 27 | iterateeArgs: MapTreeIterateeArg>, 28 | ) => TreeTypeWithChildrenField, 29 | options: MapTreeOptions = { childrenField: 'children' as ChildField }, 30 | ): TreeTypeWithChildrenField[] { 31 | const { depth = 0, parent = null, childrenField } = options || {}; 32 | const newNodes = []; 33 | 34 | for (let i = 0; i < treeNodes.length; i += 1) { 35 | const node = iteratee(treeNodes[i], { 36 | index: i, 37 | treeNodes, 38 | depth, 39 | parent, 40 | preNode: newNodes[newNodes.length - 1], 41 | }); 42 | 43 | if (node === null) { 44 | continue; 45 | } 46 | 47 | if (Array.isArray(node[childrenField])) { 48 | node[childrenField] = mapTree(node[childrenField], iteratee, { 49 | depth: depth + 1, 50 | parent: node, 51 | childrenField, 52 | }) as TreeTypeWithChildrenField[ChildField]; 53 | } 54 | 55 | newNodes.push(node); 56 | } 57 | 58 | return newNodes; 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/utils/objectEach.ts: -------------------------------------------------------------------------------- 1 | export function objectEach(input: T, callback: (input: T, inputKey: keyof T) => void) { 2 | if (typeof input !== 'object' || !input) { 3 | return; 4 | } 5 | 6 | for (const key of Object.keys(input)) { 7 | const item = input[key as keyof T]; 8 | 9 | callback(input, key as keyof T); 10 | 11 | if (typeof item === 'object') { 12 | objectEach(item as unknown as T, callback); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/wrappers/CheckChildRoute.tsx: -------------------------------------------------------------------------------- 1 | import { useOutlet } from 'react-router-dom'; 2 | import type { PropsWithChildren } from 'react'; 3 | import type React from 'react'; 4 | 5 | const CheckChildRoute: React.FC> = ({ children }) => { 6 | const outlet = useOutlet(); 7 | 8 | // 如果有子路由,不渲染当前组件 9 | if (outlet) { 10 | return outlet; 11 | } 12 | 13 | return children; 14 | }; 15 | 16 | export default CheckChildRoute; 17 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/wrappers/CheckUsersPagePermission.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Navigate } from 'react-router-dom'; 2 | 3 | import { hasPermission } from '@/apps/operator/constant/access'; 4 | 5 | const CheckUsersPagePermission = () => { 6 | if (!hasPermission('canUsersPagePermission')) { 7 | return ; 8 | } 9 | return ; 10 | }; 11 | 12 | export default CheckUsersPagePermission; 13 | -------------------------------------------------------------------------------- /frontend/src/apps/operator/wrappers/LoginCheck.tsx: -------------------------------------------------------------------------------- 1 | import type { To } from 'react-router-dom'; 2 | import { useRouteLoaderData, useNavigate } from 'react-router-dom'; 3 | 4 | // 已登录的用户访问登录页时,自动返回前一页 5 | export default function LoginCheck({ children }: { children: JSX.Element }) { 6 | const auth = useRouteLoaderData('root'); 7 | const navigate = useNavigate(); 8 | 9 | if (auth) { 10 | setTimeout(() => { 11 | navigate(-1 as To); 12 | }); 13 | 14 | return null; 15 | } 16 | 17 | return children; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/App.tsx: -------------------------------------------------------------------------------- 1 | import AppContainer from '@/components/AppContainer'; 2 | import RouterContainer from '@/components/RouterContainer'; 3 | import { QueryProvider } from '@/constant/queryClient'; 4 | 5 | import routes from './routes'; 6 | 7 | export default function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/README.md: -------------------------------------------------------------------------------- 1 | # 供应商(标注)端 2 | 3 | 使用实验室的账号体系(SSO) 4 | 5 | 附:[SSO 接入文档](https://aicarrier.feishu.cn/docx/FYdedfGzEoV73Axi2lLcQdcrnzc) 6 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/supplier/assets/bg.png -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/apps/supplier/assets/empty.png -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/join.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/timeout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/assets/title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/components/Copy/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 4 | import { Tooltip } from 'antd'; 5 | import clsx from 'clsx'; 6 | 7 | import IconFont from '@/components/IconFont'; 8 | import { message } from '@/components/StaticAnt'; 9 | import { useIntl } from 'react-intl'; 10 | 11 | interface IProps extends HTMLAttributes { 12 | val: string; 13 | children?: React.ReactNode; 14 | } 15 | 16 | const Copy: React.FC> = ({ val, className, children }) => { 17 | const { formatMessage } = useIntl(); 18 | return ( 19 | { 22 | message.success(formatMessage({ id: 'common.copy.success' })); 23 | }} 24 | > 25 | {children || ( 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | )} 39 | 40 | ); 41 | }; 42 | 43 | export default Copy; 44 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/components/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { Empty as AntdEmpty } from 'antd'; 4 | import { clsx } from 'clsx'; 5 | import type { VariantProps } from 'class-variance-authority'; 6 | import { cva } from 'class-variance-authority'; 7 | 8 | import emptyPic from '../../assets/empty.png'; 9 | 10 | const base = clsx('rounded-sm overflow-hidden'); 11 | const variants = cva(base, { 12 | variants: { 13 | size: { 14 | default: 'pt-16', 15 | }, 16 | bordered: { 17 | true: 'border border-solid border-border-secondary', 18 | }, 19 | }, 20 | defaultVariants: { 21 | size: 'default', 22 | }, 23 | }); 24 | 25 | interface IProps extends HTMLAttributes, VariantProps { 26 | description?: React.ReactNode | string; 27 | imageHeight?: number; 28 | } 29 | 30 | const Empty: React.FC> = ({ className, bordered, description, imageHeight }) => { 31 | return ( 32 |
33 | 34 |
35 | ); 36 | }; 37 | 38 | export default Empty; 39 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/access.ts: -------------------------------------------------------------------------------- 1 | import type { IUserInfo } from '@/api/user'; 2 | import { EUserRole } from '@/api/user'; 3 | import queryClient from '@/constant/queryClient'; 4 | import { userInfoKey } from '@/constant/query-key-factories'; 5 | 6 | export interface IAccessValue { 7 | canReadPage: boolean; // 是否能访问页面 8 | canReadMember: boolean; // 是否能访问成员管理页面 9 | } 10 | 11 | // 用户团队权限 12 | 13 | export const accessObj: Record = { 14 | [EUserRole.admin]: '管理员', 15 | [EUserRole.user]: '普通用户', 16 | }; 17 | 18 | type UserRoleMap = Record; 19 | 20 | const roleAccessMap: UserRoleMap = { 21 | [EUserRole.admin]: { 22 | canReadPage: true, 23 | canReadMember: true, 24 | }, 25 | [EUserRole.user]: { 26 | canReadPage: true, 27 | canReadMember: false, 28 | }, 29 | }; 30 | 31 | // 校验当前用户的团队权限 32 | export function hasPermission(permission: keyof IAccessValue) { 33 | const user = queryClient.getQueryData(userInfoKey.all); 34 | const role = user?.role || EUserRole.user; 35 | return roleAccessMap[role][permission]; 36 | } 37 | 38 | export default roleAccessMap; 39 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/operatorAccess.ts: -------------------------------------------------------------------------------- 1 | import type { IUserInfo } from '@/api/user'; 2 | import { EUserRole } from '@/api/user'; 3 | import queryClient from '@/constant/queryClient'; 4 | import { userInfoKey } from '@/constant/query-key-factories'; 5 | 6 | // 运营权限 7 | export interface IAccessValue { 8 | canReadPage: boolean; // 是否能访问页面 9 | canReadPreview: boolean; // 是否能访问预览页面 10 | } 11 | 12 | type UserRoleMap = Record; 13 | 14 | const roleAccessMap: UserRoleMap = { 15 | [EUserRole.admin]: { 16 | canReadPage: true, 17 | canReadPreview: true, 18 | }, 19 | [EUserRole.user]: { 20 | canReadPage: true, 21 | canReadPreview: false, 22 | }, 23 | }; 24 | 25 | // 权限验证 26 | export function hasPermission(permission: keyof IAccessValue) { 27 | const user = queryClient.getQueryData(userInfoKey.all); 28 | if (!user?.role) return false; 29 | return roleAccessMap[user?.role][permission]; 30 | } 31 | 32 | export default roleAccessMap; 33 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/query-key-factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './task'; 2 | export * from './member'; 3 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/query-key-factories/member.ts: -------------------------------------------------------------------------------- 1 | // 任务列表 2 | export const memberKey = { 3 | all: ['member_key'] as const, 4 | lists: () => [...memberKey.all, 'memberKey'] as const, 5 | list: (filter: any) => [...memberKey.lists(), filter] as const, 6 | details: () => [...memberKey.all, 'details'] as const, 7 | detail: (id: string | number) => [...memberKey.details(), id] as const, 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/query-key-factories/task.ts: -------------------------------------------------------------------------------- 1 | import type { IPagination } from '@/apps/supplier/services/task'; 2 | 3 | // 任务列表 4 | export const taskKey = { 5 | all: ['task_key'] as const, 6 | lists: () => [...taskKey.all, 'list'] as const, 7 | list: (filter: IPagination) => [...taskKey.lists(), filter] as const, 8 | details: () => [...taskKey.all, 'details'] as const, 9 | detail: (id: string | number) => [...taskKey.details(), id] as const, 10 | }; 11 | 12 | // 问题列表 13 | export const questionKey = { 14 | all: ['question_key'] as const, 15 | lists: () => [...questionKey.all, 'list'] as const, 16 | list: (filter: string) => [...questionKey.lists(), filter] as const, 17 | details: () => [...questionKey.all, 'details'] as const, 18 | detail: (obj: any) => [...questionKey.details(), obj] as const, 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/constant/task.ts: -------------------------------------------------------------------------------- 1 | export enum ERouterTaskType { 2 | task = 'task', 3 | preview = 'preview', 4 | review = 'review', // 预览全部任务 5 | reviewTask = 'review_task', // 预览某个标注员任务 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/hooks/useTaskParams.ts: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import useUrlState from '@ahooksjs/use-url-state'; 3 | 4 | import { ERouterTaskType } from '@/apps/supplier/constant/task'; 5 | import type { ERecordStatus } from '@/apps/supplier/services/task'; 6 | 7 | /** 8 | * type 当前任务类型 9 | * taskId 任务id 10 | * isPreview 是否是预览任务 11 | * */ 12 | 13 | export enum EQueryQuestionType { 14 | all = 'all', 15 | problem = 'problem', 16 | customize = 'customize', 17 | } 18 | // 源题组合展示 默认不传 单题模式 19 | export enum EKind { 20 | with_duplicate = 'with_duplicate', 21 | } 22 | 23 | interface IQuery { 24 | user_id?: string; 25 | // 全部题目 未达标的题录 26 | record_status?: ERecordStatus; 27 | flow_index?: string; 28 | // 是否是搜索 29 | is_search?: string; 30 | data_id?: string; // 题目 id 31 | questionnaire_id?: string; // 问卷 id 32 | // 题目类型 包含 全部题目 仅看标为有问题 自定义题目范围 33 | question_type?: EQueryQuestionType; 34 | kind?: EKind; // 源题组合展示 默认不传 单题模式 35 | inlet?: 'supplier' | 'operator'; 36 | } 37 | 38 | // 预览任务 https://labelu-llm-dev.shlab.tech/supplier/preview/beebc1fa-9b7f-406a-b498-194dab40d673; 39 | // 查看题目 https://labelu-llm-dev.shlab.tech/supplier/review/beebc1fa-9b7f-406a-b498-194dab40d673; 40 | // 查看标注员标注任务 https://labelu-llm-dev.shlab.tech/supplier/review_task/beebc1fa-9b7f-406a-b498-194dab40d673?user_id=1011; 41 | 42 | export const useTaskParams = () => { 43 | const params = useParams<{ type: ERouterTaskType; id: string }>(); 44 | const [urlState, setUrlState] = useUrlState({ 45 | flow_index: undefined, 46 | user_id: undefined, 47 | record_status: undefined, 48 | is_search: undefined, 49 | data_id: undefined, 50 | inlet: undefined, 51 | }); 52 | 53 | const type = params.type || ERouterTaskType.task; 54 | 55 | return { 56 | type, 57 | taskId: params.id, 58 | isPreview: [ERouterTaskType.preview, ERouterTaskType.review, ERouterTaskType.reviewTask].includes(type), 59 | flow_index: urlState.flow_index, 60 | urlState, 61 | setUrlState, 62 | } as const; 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/hooks/useUserInfo.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { getUserInfo } from '@/api/user'; 4 | import { userInfoKey } from '@/constant/query-key-factories'; 5 | 6 | export const useUserInfo = () => { 7 | const { data: userInfo } = useQuery({ 8 | queryKey: userInfoKey.all, 9 | queryFn: async () => getUserInfo(), 10 | staleTime: Infinity, 11 | gcTime: Infinity, 12 | }); 13 | return { 14 | userInfo, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 23 | LabelU-LLM 24 | 25 | 26 | 27 | 28 |
29 | 30 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | 3 | import App from './App'; 4 | import '../../initialize'; 5 | import './styles/index.css'; 6 | import 'antd/dist/reset.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render(); 9 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/layouts/CustomPageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { PageContainer } from '@ant-design/pro-components'; 2 | import type { PropsWithChildren } from 'react'; 3 | import React from 'react'; 4 | import { clsx } from 'clsx'; 5 | 6 | interface IProps { 7 | className?: string; 8 | title?: string | React.ReactNode; 9 | bodyClassName?: string; 10 | } 11 | 12 | const CustomPageContainer: React.FC> = ({ className, title, children, bodyClassName }) => { 13 | return ( 14 | 20 |
{children}
21 |
22 | ); 23 | }; 24 | 25 | export default CustomPageContainer; 26 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/layouts/index.css: -------------------------------------------------------------------------------- 1 | /* 菜单 item 背景圆角 */ 2 | .layout-wrapper .ant-menu-item { 3 | @apply rounded-none; 4 | } 5 | 6 | .layout-wrapper .ant-pro-sider-logo { 7 | @apply !py-5 !px-8; 8 | 9 | height: var(--header-height); 10 | } 11 | 12 | .layout-wrapper .ant-layout-sider-children { 13 | @apply !p-0; 14 | } 15 | 16 | .layout-wrapper .ant-pro-sider-menu { 17 | @apply !p-4; 18 | } 19 | 20 | .layout-wrapper .ant-menu-inline-collapsed { 21 | @apply !p-1; 22 | } 23 | 24 | .layout-wrapper .ant-pro-sider-logo img { 25 | @apply !h-7; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import type { PropsWithChildren } from 'react'; 3 | import React from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | const ResultBox: React.FC = () => { 7 | const navigate = useNavigate(); 8 | return ( 9 |
10 | navigate('/task')}> 16 | Back Task 17 | 18 | } 19 | /> 20 |
21 | ); 22 | }; 23 | 24 | export default ResultBox; 25 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/join-team/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { useParams, useNavigate } from 'react-router-dom'; 4 | import { Button, Spin } from 'antd'; 5 | import { useQuery } from '@tanstack/react-query'; 6 | 7 | import { getInviteLinkDetail, joinTeam } from '@/apps/supplier/services/joinTeam'; 8 | 9 | import { ReactComponent as JoinImg } from '../../assets/join.svg'; 10 | import { ReactComponent as Timeout } from '../../assets/timeout.svg'; 11 | import { useIntl } from 'react-intl'; 12 | 13 | type IProps = HTMLAttributes; 14 | 15 | const Team: React.FC> = () => { 16 | const { formatMessage } = useIntl(); 17 | const params = useParams<{ id: string }>(); 18 | const navigate = useNavigate(); 19 | const { data, isLoading, refetch } = useQuery({ 20 | queryKey: ['getInviteLinkDetail', params.id], 21 | queryFn: async () => getInviteLinkDetail(params.id as string), 22 | }); 23 | 24 | const toTask = async () => { 25 | await joinTeam({ team_id: data?.team_id as string }); 26 | refetch?.(); 27 | }; 28 | 29 | return ( 30 | 31 |
32 | {data?.is_expired ? : } 33 |
34 | {formatMessage({ id: 'member.team' })}:{data?.team_name} 35 |
36 |
{formatMessage({ id: 'member.team.join' })}
37 | {data?.is_expired && ( 38 | 41 | )} 42 | {!data?.is_expired && data?.is_joined && ( 43 | 46 | )} 47 | {!data?.is_expired && !data?.is_joined && ( 48 | 51 | )} 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default Team; 58 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/task.[id]/Answer/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { clsx } from 'clsx'; 4 | 5 | import type { IQuestion } from '@/apps/supplier/services/task'; 6 | 7 | import Widget from '../Widget'; 8 | 9 | type IProps = HTMLAttributes & { 10 | name: string; 11 | conversation?: IQuestion[]; 12 | }; 13 | 14 | const Answer: React.FC> = ({ className, name, conversation }) => { 15 | if (!conversation?.length) { 16 | return null; 17 | } 18 | return ( 19 |
20 | {conversation?.map((item) => { 21 | return ; 22 | })} 23 |
24 | ); 25 | }; 26 | 27 | export default React.memo(Answer); 28 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/task.[id]/CheckTaskType/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | 4 | import { useTaskParams } from '@/apps/supplier/hooks/useTaskParams'; 5 | import type { ERouterTaskType } from '@/apps/supplier/constant/task'; 6 | 7 | type IProps = HTMLAttributes & { 8 | types: ERouterTaskType[]; 9 | }; 10 | 11 | const CheckTaskType: React.FC> = ({ types, children }) => { 12 | const { type } = useTaskParams(); 13 | if (types.includes(type)) return <>{children}; 14 | return null; 15 | }; 16 | 17 | export default CheckTaskType; 18 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/task.[id]/Countdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLAttributes, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { Statistic, Tooltip } from 'antd'; 4 | import { QuestionCircleOutlined } from '@ant-design/icons'; 5 | import { FormattedMessage, useIntl } from 'react-intl'; 6 | 7 | const { Countdown } = Statistic; 8 | 9 | interface IProps extends HTMLAttributes { 10 | remain_time?: number; 11 | whetherTimeout: () => void; 12 | } 13 | 14 | const CountdownBox: React.FC> = ({ remain_time, whetherTimeout }) => { 15 | const { formatMessage } = useIntl(); 16 | return ( 17 | 22 | 23 | 24 | 25 | 26 | 27 | } 28 | format="mm:ss" 29 | value={(remain_time ?? 0) * 1000} 30 | onFinish={whetherTimeout} 31 | /> 32 | ); 33 | }; 34 | 35 | export default CountdownBox; 36 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/task.[id]/CustomizeTextarea/upload.ts: -------------------------------------------------------------------------------- 1 | import type { RcFile } from 'antd/es/upload'; 2 | 3 | import { getUploadUrl } from '@/apps/supplier/services/task'; 4 | import request from '@/api/request'; 5 | 6 | export const upload = async (file: RcFile) => { 7 | const { get_url, put_url } = await getUploadUrl({ 8 | type: file.type, 9 | content_length: file.size, 10 | suffix: file.name.split('.').pop()?.toLowerCase() || '', 11 | }); 12 | await request.put(put_url, file, { headers: { 'Content-Type': file.type, 'Content-Length': file.size } }); 13 | return get_url; 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/apps/supplier/pages/task.[id]/QuestionnaireSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | import { Select } from 'antd'; 4 | 5 | import { ERecordStatus, getTaskDataIds } from '@/apps/supplier/services/task'; 6 | import { EKind, useTaskParams } from '@/apps/supplier/hooks/useTaskParams'; 7 | import { FormattedMessage, useIntl } from 'react-intl'; 8 | 9 | const QuestionnaireSelect = ({ data_id, questionnaire_id }: { data_id?: string; questionnaire_id?: string }) => { 10 | const { formatMessage } = useIntl(); 11 | const { urlState, taskId, setUrlState } = useTaskParams(); 12 | const { data } = useQuery({ 13 | queryKey: ['getTaskDataIds', taskId, questionnaire_id], 14 | queryFn: async () => 15 | getTaskDataIds({ 16 | task_id: taskId!, 17 | questionnaire_id: questionnaire_id, 18 | record_status: urlState.record_status === ERecordStatus.invalid ? ERecordStatus.invalid : undefined, 19 | }), 20 | enabled: urlState.kind === EKind.with_duplicate && !!questionnaire_id, 21 | }); 22 | if (urlState.kind !== EKind.with_duplicate) { 23 | return null; 24 | } 25 | 26 | const options = data?.data?.map((item) => ({ 27 | value: item, 28 | label: item, 29 | })); 30 | return ( 31 |
32 | 33 | : 34 | 35 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/components/FancyInput/base/Number.fancy.tsx: -------------------------------------------------------------------------------- 1 | import type { InputNumberProps } from 'antd'; 2 | import { InputNumber } from 'antd'; 3 | 4 | export type FancyNumberProps = InputNumberProps & { 5 | fieldProps?: any; 6 | }; 7 | 8 | export function FancyNumber({ fieldProps, ...rest }: FancyNumberProps) { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/FancyInput/base/String.fancy.tsx: -------------------------------------------------------------------------------- 1 | import type { InputProps } from 'antd'; 2 | import { Input } from 'antd'; 3 | import type { TextAreaProps } from 'antd/es/input'; 4 | 5 | export interface FancyStringProps extends InputProps { 6 | alias?: 'input' | 'textarea'; 7 | fieldProps?: any; 8 | antProps?: any; 9 | fullField?: any; 10 | } 11 | 12 | export function FancyString({ alias = 'input', fieldProps, antProps, fullField, ...rest }: FancyStringProps) { 13 | if (alias === 'input') { 14 | return ; 15 | } 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/components/FancyInput/fancyInput.ts: -------------------------------------------------------------------------------- 1 | import { FancyBoolean } from './base/Boolean.fancy'; 2 | import { FancyEnum } from './base/Enum.fancy'; 3 | import { FancyString } from './base/String.fancy'; 4 | import { FancyNumber } from './base/Number.fancy'; 5 | 6 | export const inputs: Record> = { 7 | enum: FancyEnum, 8 | string: FancyString, 9 | number: FancyNumber, 10 | boolean: FancyBoolean, 11 | }; 12 | 13 | export const inputMapping: Record = { 14 | string: '字符串', 15 | number: '数字', 16 | boolean: '布尔值', 17 | enum: '选择', 18 | }; 19 | 20 | export function add(type: string, component: React.FC) { 21 | if (inputs[type]) { 22 | console.warn(`[FancyInput] ${type} already exists`); 23 | return; 24 | } 25 | 26 | inputs[type] = component; 27 | } 28 | 29 | export function remove(type: string) { 30 | delete inputs[type]; 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/FancyInput/index.tsx: -------------------------------------------------------------------------------- 1 | import { inputs, add, remove } from './fancyInput'; 2 | import type { FancyInputProps } from './types'; 3 | 4 | export default function FancyInput({ 5 | type, 6 | field, 7 | key, 8 | label, 9 | hidden, 10 | rules, 11 | tooltip, 12 | dependencies, 13 | ...props 14 | }: FancyInputProps) { 15 | const Input = inputs[type]; 16 | 17 | if (!Input) { 18 | console.warn(`FancyInput: ${type} is not supported`); 19 | return <>Not supported yet; 20 | } 21 | 22 | return ; 23 | } 24 | 25 | export { add, remove }; 26 | -------------------------------------------------------------------------------- /frontend/src/components/FancyInput/types.ts: -------------------------------------------------------------------------------- 1 | import type { FormInstance, FormItemProps, Rule } from 'antd/es/form'; 2 | import type { NamePath } from 'antd/es/form/interface'; 3 | 4 | export interface FancyInputParams { 5 | /** form field type */ 6 | type: string; 7 | /** form field name */ 8 | field: string; 9 | /** uniq key */ 10 | key: string; 11 | label?: React.ReactNode; 12 | disabled?: boolean; 13 | initialValue?: any; 14 | children?: FancyInputParams[]; 15 | hidden?: boolean; 16 | tooltip?: string; 17 | rules?: Rule[]; 18 | layout?: 'horizontal' | 'vertical'; 19 | dependencies?: (string | number)[]; 20 | fieldProps?: FormItemProps; 21 | renderFormItem?: (params: FancyInputParams, form: FormInstance, fullField: NamePath) => React.ReactNode; 22 | renderGroup?: (params: FancyInputParams, form: FormInstance, fullField: NamePath) => React.ReactNode; 23 | /** antd input component props, only in template definition */ 24 | antProps?: Record; 25 | } 26 | 27 | export interface FancyInputProps { 28 | type: string; 29 | [key: string]: any; 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/components/Help.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons'; 2 | import { Tooltip } from 'antd'; 3 | import type { TooltipProps } from 'antd'; 4 | import React from 'react'; 5 | 6 | export default function Help({ 7 | children, 8 | placement, 9 | className, 10 | }: React.PropsWithChildren<{ 11 | placement?: TooltipProps['placement']; 12 | className?: string; 13 | }>) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/IconFont/index.tsx: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from '@ant-design/icons'; 2 | 3 | const IconFont = createFromIconfontCN({ 4 | scriptUrl: `/iconfont_3732290.js`, 5 | }); 6 | 7 | export default IconFont; 8 | -------------------------------------------------------------------------------- /frontend/src/components/Markdown/errorImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/components/Markdown/errorImage.png -------------------------------------------------------------------------------- /frontend/src/components/MemberInvite/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Alert, Spin } from 'antd'; 3 | import { RedoOutlined, InfoCircleOutlined } from '@ant-design/icons'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | import dayjs from 'dayjs'; 6 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 7 | 8 | import { message } from '@/components/StaticAnt'; 9 | import { createInviteLink } from '@/api/team'; 10 | 11 | interface IProps { 12 | teamId: string; 13 | } 14 | 15 | const Invite = ({ teamId }: IProps) => { 16 | const { data, refetch, isLoading, isFetching } = useQuery({ 17 | queryKey: ['createInviteLink', teamId], 18 | queryFn: async () => createInviteLink({ team_id: teamId as string }), 19 | }); 20 | 21 | const url = data?.link_id ? window.location.origin + '/supplier/join/' + data?.link_id : ''; 22 | return ( 23 | <> 24 | } 30 | /> 31 |
操作方式:将链接发给成员,成员点击链接通过邀请加入团队
32 | 33 |
34 |
{url}
35 | { 38 | message.success('复制成功'); 39 | }} 40 | > 41 | 44 | 45 |
46 |
47 |
48 | { 51 | if (!isLoading) { 52 | refetch?.(); 53 | } 54 | }} 55 | > 56 | 57 | 重新生成 58 | 59 | 有效期至 {dayjs(data?.expire_time).format('YYYY-MM-DD HH:mm:ss')} 60 |
61 | 62 | ); 63 | }; 64 | 65 | export default Invite; 66 | -------------------------------------------------------------------------------- /frontend/src/components/MessageBox/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import type { VariantProps } from 'class-variance-authority'; 4 | import { cva } from 'class-variance-authority'; 5 | import clsx from 'clsx'; 6 | 7 | const base = 'rounded-md inline-block py-2 px-5'; 8 | const variants = cva(base, { 9 | variants: { 10 | type: { 11 | default: 'bg-white', 12 | secondary: 'chat-secondary', 13 | primary: 'chat-primary', 14 | }, 15 | bordered: { 16 | true: 'border border-solid border-borderSecondary', 17 | }, 18 | }, 19 | defaultVariants: { 20 | type: 'default', 21 | }, 22 | }); 23 | // 消除框的基本样式 24 | interface IMessageBox extends VariantProps { 25 | className?: string; 26 | } 27 | const MessageBox: React.FC> = ({ type, bordered, className, children }) => { 28 | return
{children}
; 29 | }; 30 | 31 | export default MessageBox; 32 | -------------------------------------------------------------------------------- /frontend/src/components/StaticAnt.tsx: -------------------------------------------------------------------------------- 1 | import { App } from 'antd'; 2 | import type { MessageInstance } from 'antd/es/message/interface'; 3 | import type { ModalStaticFunctions } from 'antd/es/modal/confirm'; 4 | import type { NotificationInstance } from 'antd/es/notification/interface'; 5 | 6 | let message: MessageInstance; 7 | let notification: NotificationInstance; 8 | let modal: Omit; 9 | 10 | export default () => { 11 | const staticFunction = App.useApp(); 12 | message = staticFunction.message; 13 | modal = staticFunction.modal; 14 | notification = staticFunction.notification; 15 | return null; 16 | }; 17 | 18 | export { message, notification, modal }; 19 | -------------------------------------------------------------------------------- /frontend/src/constant/chat.ts: -------------------------------------------------------------------------------- 1 | export const model = { 2 | name: 'PuYu000', 3 | description: '基于浦江PJLM-0进行了人类对齐训练的模型', 4 | }; 5 | 6 | export enum EPlugins { 7 | calculator = 'calculator', 8 | equation = 'equation', 9 | search = 'search', 10 | } 11 | 12 | export const plugins = { 13 | [EPlugins.calculator]: { 14 | value: EPlugins.calculator, 15 | label: '计算器', 16 | desc: '执行加减乘除等数值运算', 17 | }, 18 | [EPlugins.equation]: { 19 | value: EPlugins.equation, 20 | label: '解方程器', 21 | desc: '求解带有未知数的方程', 22 | }, 23 | [EPlugins.search]: { 24 | value: EPlugins.search, 25 | label: '搜索', 26 | desc: '实时搜索引擎搜索', 27 | }, 28 | }; 29 | export enum EUserType { 30 | user = 'user', 31 | robot = 'robot', 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/constant/query-key-factories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | -------------------------------------------------------------------------------- /frontend/src/constant/query-key-factories/user.ts: -------------------------------------------------------------------------------- 1 | // sso登录的用户信息 2 | export const userInfoKey = { 3 | all: ['ssoUserInfo'] as const, 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/constant/queryClient.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 5 | 6 | // 创建一个 client 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | retry: false, 11 | refetchOnWindowFocus: false, 12 | }, 13 | }, 14 | }); 15 | 16 | export const QueryProvider: React.FC = (props) => { 17 | return ( 18 | 19 | {props.children} 20 | {import.meta.env.DEV && } 21 | 22 | ); 23 | }; 24 | 25 | export default queryClient; 26 | -------------------------------------------------------------------------------- /frontend/src/constant/team.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TEAM = '00000000-0000-0000-0000-000000000000'; 2 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLang.ts: -------------------------------------------------------------------------------- 1 | import { useQueryClient, useQuery } from '@tanstack/react-query'; 2 | import store from 'storejs'; 3 | 4 | const UseLang = () => { 5 | const queryClient = useQueryClient(); 6 | 7 | const query = useQuery({ 8 | queryKey: ['lang'], 9 | queryFn: async () => store.get('lang') || 'zh-CN', 10 | }); 11 | // 更新 12 | const setLang = (value: 'zh-CN' | 'en-US') => { 13 | store.set('lang', value); 14 | queryClient.refetchQueries({ queryKey: ['lang'] }); 15 | }; 16 | return { setLang, lang: query.data, isZh: query.data === 'zh-CN' }; 17 | }; 18 | 19 | export default UseLang; 20 | -------------------------------------------------------------------------------- /frontend/src/hooks/useStoreIds.ts: -------------------------------------------------------------------------------- 1 | import { message } from '@/components/StaticAnt'; 2 | import { useIntl } from 'react-intl'; 3 | import store from 'storejs'; 4 | 5 | export type TStoreKey = 'data_id' | 'questionnaire_id'; 6 | export const useStoreIds = () => { 7 | const { formatMessage } = useIntl(); 8 | // 保存 ids 区分 9 | const saveIds = async (key: TStoreKey, val: string) => { 10 | // val 转数组 过滤空字符串 11 | store.set( 12 | key, 13 | val 14 | .split('\n') 15 | ?.map((item) => item.trim()) 16 | .filter(Boolean), 17 | ); 18 | }; 19 | 20 | const getIds = (key: TStoreKey) => { 21 | return store.get(key) || []; 22 | }; 23 | 24 | // 上一题 下一题 25 | const nextId = (id: string, key: TStoreKey, n: 1 | -1) => { 26 | const ids = getIds(key); 27 | // 获取当前 id 下标 28 | const index = ids.indexOf(id); 29 | // 获取上一个或下一个id 30 | const nextIndex = index + n; 31 | // 检查索引是否越界 32 | if (nextIndex < 0 || nextIndex >= ids.length) { 33 | message.warning(formatMessage({ id: 'task.error.msg2' })); 34 | return null; 35 | } 36 | 37 | return ids[nextIndex]; 38 | }; 39 | 40 | const clearAll = () => { 41 | // 退出当前页面 关闭tab都需要清除浏览器缓存 缓存的 id 42 | store.remove('data_id'); 43 | store.remove('questionnaire_id'); 44 | }; 45 | 46 | return { saveIds, getIds, clearAll, nextId }; 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/initialize.tsx: -------------------------------------------------------------------------------- 1 | import type { TooltipPropsWithTitle } from 'antd/lib/tooltip'; 2 | import React from 'react'; 3 | 4 | /** 5 | * 此文件用于应用初始化时的准备工作,比如一些注册处理函数,库的初始配置等 6 | */ 7 | 8 | declare global { 9 | interface Window { 10 | // 是否开发环境 11 | DEV: boolean; 12 | } 13 | } 14 | 15 | // ==========================【formatter】========================= 16 | 17 | interface EllipseOption extends TooltipPropsWithTitle { 18 | maxWidth?: Pick | string; 19 | maxLength: number; 20 | type?: 'tooltip' | 'popover'; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/loaders/sso.loader.ts: -------------------------------------------------------------------------------- 1 | import type { LoaderFunctionArgs } from 'react-router-dom'; 2 | 3 | import { getUserInfo } from '@/api/user'; 4 | import queryClient from '@/constant/queryClient'; 5 | import { userInfoKey } from '@/constant/query-key-factories'; 6 | 7 | export async function ssoLoader({ request }: LoaderFunctionArgs) { 8 | try { 9 | // 往react-router中注入用户信息 10 | return await queryClient.fetchQuery({ 11 | queryKey: userInfoKey.all, 12 | queryFn: getUserInfo, 13 | staleTime: Infinity, 14 | gcTime: Infinity, 15 | }); 16 | } catch (err) { 17 | console.error(err); 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import zhCN from './zh-CN'; 2 | import enUS from './en-US'; 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | 'en-US': enUS, 7 | } as Record; 8 | -------------------------------------------------------------------------------- /frontend/src/styles/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/styles/font/iconfont.ttf -------------------------------------------------------------------------------- /frontend/src/styles/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/styles/font/iconfont.woff -------------------------------------------------------------------------------- /frontend/src/styles/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opendatalab/LabelLLM/11f2a221f73c9625ec820bec8fc158c1ae16b8a9/frontend/src/styles/font/iconfont.woff2 -------------------------------------------------------------------------------- /frontend/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/src/styles/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": { 3 | "blue": "#0D53DE", 4 | "colorPrimary": "#0D53DE", 5 | "colorSuccess": "#00b365", 6 | "colorWarning": "#ff8800", 7 | "colorError": "#f5483b", 8 | "colorText": "rgba(18, 19, 22, 0.8)", 9 | "colorTextSecondary": "rgba(18, 19, 22, 0.5)", 10 | "colorTextTertiary": "rgba(18, 19, 22, 0.25)", 11 | "colorTextQuaternary": "rgba(18, 19, 22, 0.20)", 12 | "colorBorder": "#D7D8DD", 13 | "borderRadius": 6, 14 | "colorBorderSecondary": "#EBECF0", 15 | "colorInfo": "#0D53DE", 16 | "boxShadow": " 0 8px 26px 0 rgba(18, 19, 22, 0.12), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) ", 17 | "boxShadowSecondary": " 0 4px 12px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05) ", 18 | "wireframe": false 19 | }, 20 | "components": { 21 | "Button": { 22 | "controlHeight": 36, 23 | "marginXS": 4 24 | }, 25 | "Input": { 26 | "colorBorder": "#EBECF0", 27 | "controlHeight": 36 28 | }, 29 | "Select": { 30 | "colorBorder": "#EBECF0", 31 | "controlHeight": 36 32 | }, 33 | "Table": { 34 | "borderRadiusLG": 8, 35 | "padding": 24 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/utils/getUrlExtension.ts: -------------------------------------------------------------------------------- 1 | export function getUrlExtension(url: string): string { 2 | const urlWithoutQuery = url.split('?')[0]; 3 | return urlWithoutQuery.split('.').pop()?.toLowerCase() || ''; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/utils/gid.ts: -------------------------------------------------------------------------------- 1 | export function gid() { 2 | return `_${Math.random().toString(36).substr(2, 9)}`; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/utils/parseDocumentType.ts: -------------------------------------------------------------------------------- 1 | export function parseDocumentType(url: string) { 2 | const urlWithoutQuery = url.split('?')[0]; 3 | const extension = urlWithoutQuery.split('.').pop(); 4 | console.log(extension); 5 | return extension?.toLowerCase() || ''; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/utils/sso.ts: -------------------------------------------------------------------------------- 1 | /** 前往登录页 */ 2 | export function goLogin() { 3 | // window.location.href = getUAA(sso?.login || ''); 4 | window.location.href = '/login'; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /frontend/src/wrappers/RequireSSO.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteLoaderData } from 'react-router-dom'; 2 | 3 | import { goLogin } from '@/utils/sso'; 4 | 5 | export default function RequireAuth({ children }: { children: JSX.Element }) { 6 | const auth = useRouteLoaderData('root'); 7 | 8 | if (!auth) { 9 | goLogin(); 10 | 11 | return null; 12 | } 13 | 14 | return children; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: '#0d53de', 8 | success: '#00b365', 9 | warning: '#ff8800', 10 | error: '#f5483b', 11 | 'base-color': '#121316', 12 | color: 'rgba(18, 19, 22, 0.8)', 13 | secondary: 'rgba(18, 19, 22, 0.5)', 14 | tertiary: 'rgba(18, 19, 22, 0.25)', 15 | quaternary: 'rgba(18, 19, 22, 0.20)', 16 | // ICON 颜色 17 | icon: '#4A4653', 18 | fill: '#b4b6bc', // 一级填充色 19 | 'fill-secondary': '#EBECF0', // 二级填充色 20 | 'fill-tertiary': '#F4F5F9', // 三级填充色 21 | 'fill-quaternary': '#f9f9f9', // 四级填充色 22 | colorBorder: '#D7D8DD', 23 | borderSecondary: '#EBECF0', 24 | borderRadius: 2, 25 | }, 26 | transitionTimingFunction: { 27 | 'in-expo': 'cubic-bezier(0.95, 0.05, 0.795, 0.035)', 28 | 'out-expo': 'cubic-bezier(0.19, 1, 0.22, 1)', 29 | }, 30 | }, 31 | }, 32 | plugins: [], 33 | corePlugins: { 34 | preflight: false, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "esm": true, 4 | "compilerOptions": { 5 | "module": "CommonJS", 6 | "esModuleInterop": true, 7 | "moduleResolution": "node" 8 | } 9 | }, 10 | "compilerOptions": { 11 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 12 | "module": "ESNext", 13 | "target": "ES2015", 14 | "composite": false, 15 | "declaration": true, 16 | "declarationMap": false, 17 | "downlevelIteration": true, 18 | "esModuleInterop": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "inlineSources": false, 21 | "isolatedModules": true, 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "preserveWatchOutput": true, 27 | "skipLibCheck": true, 28 | "strict": true, 29 | "baseUrl": "./", 30 | "jsx": "react-jsx", 31 | "allowJs": true, 32 | "noEmit": true, 33 | "noFallthroughCasesInSwitch": true, 34 | "allowSyntheticDefaultImports": true, 35 | "types": ["node", "vite/client"], 36 | "paths": { 37 | "@/*": ["./src/*"] 38 | } 39 | }, 40 | "include": ["src", "scripts", ".eslintrc.js", "vite.config.dev.ts", "vite.config.prod.ts", "rollup.config.ts"], 41 | "exclude": ["node_modules", "**/*.test.tsx", "**/*.test.ts"] 42 | } 43 | -------------------------------------------------------------------------------- /frontend/tsconfig.util.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2022"], 4 | "module": "CommonJS", 5 | "target": "ES2015", 6 | "composite": false, 7 | "declaration": false, 8 | "declarationMap": false, 9 | "downlevelIteration": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "inlineSources": false, 13 | "isolatedModules": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": false, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "preserveWatchOutput": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "baseUrl": "./", 22 | "jsx": "react-jsx", 23 | "allowJs": true, 24 | "noEmit": true 25 | }, 26 | "include": ["scripts"], 27 | "exclude": ["node_modules", "**/*.test.tsx", "**/*.test.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /frontend/vite.config.dev.ts: -------------------------------------------------------------------------------- 1 | import { join, relative, resolve } from 'path'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | import svgr from 'vite-plugin-svgr'; 6 | import { ViteEjsPlugin } from 'vite-plugin-ejs'; 7 | 8 | const appDir = process.env.APP_DIR || 'src/apps/login'; 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig({ 12 | base: '/', 13 | publicDir: resolve(__dirname, 'public'), 14 | server: { 15 | host: true, 16 | port: 3000, 17 | proxy: { 18 | '/api': { 19 | target: 'http://10.6.16.145:16666/', 20 | changeOrigin: true, 21 | }, 22 | }, 23 | }, 24 | 25 | optimizeDeps: { 26 | include: ['react/jsx-runtime'], 27 | }, 28 | css: { 29 | modules: { 30 | localsConvention: 'camelCaseOnly', 31 | }, 32 | }, 33 | 34 | plugins: [ 35 | react(), 36 | svgr(), 37 | ViteEjsPlugin({ 38 | // root: resolve(__dirname, appDir), 39 | root: join(__dirname, relative(__dirname, resolve(appDir))), 40 | }), 41 | ].filter(Boolean), 42 | 43 | resolve: { 44 | alias: { 45 | '@': resolve(__dirname, 'src/'), 46 | }, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## Latest Changes 4 | --------------------------------------------------------------------------------