├── .commitlintrc.js ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug_report.yml │ ├── 2_feature_request.yml │ ├── 3_question.yml │ └── 4_other.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── auto-merge.yml │ ├── docker.yml │ ├── issue-auto-comments.yml │ ├── issue-check-inactive.yml │ ├── issue-close-require.yml │ ├── issue-remove-inactive.yml │ └── lint.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── Dockerfile ├── LICENSE ├── README.md ├── build.dockerfile ├── build.sh ├── kiwi-config.json ├── messages ├── en │ ├── AccountClient.ts │ ├── BtnList.ts │ ├── BtnsBlock.ts │ ├── Chats.ts │ ├── Conversation.ts │ ├── DataControlClient.ts │ ├── SideBar.ts │ ├── SideBarHeader.ts │ ├── UserInfoClient.ts │ ├── app.ts │ ├── callback.ts │ ├── components.ts │ ├── create.ts │ ├── index.ts │ ├── oidc.ts │ ├── setting.ts │ └── utils.ts └── zh │ ├── AccountClient.ts │ ├── BtnList.ts │ ├── BtnsBlock.ts │ ├── Chats.ts │ ├── Conversation.ts │ ├── DataControlClient.ts │ ├── SideBar.ts │ ├── SideBarHeader.ts │ ├── UserInfoClient.ts │ ├── app.ts │ ├── callback.ts │ ├── components.ts │ ├── create.ts │ ├── index.ts │ ├── oidc.ts │ ├── setting.ts │ └── utils.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public ├── 404 │ ├── 404.jpg │ └── 4xx.jpg ├── create_gpt.png ├── default_chat.png ├── discover.png ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ └── icon-512x512.png ├── manifest.json └── svg │ └── logo.svg ├── server.mjs ├── src ├── app │ ├── [locale] │ │ ├── [...rest] │ │ │ └── page.tsx │ │ ├── agent │ │ │ ├── components │ │ │ │ ├── TagContent │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ └── page.tsx │ │ ├── chat │ │ │ ├── (conversation) │ │ │ │ └── index.tsx │ │ │ ├── [id] │ │ │ │ └── page.tsx │ │ │ ├── bot │ │ │ │ └── create │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── styles.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── logout │ │ │ └── page.tsx │ │ ├── metadata.ts │ │ ├── not-found.tsx │ │ ├── oidc │ │ │ ├── auth │ │ │ │ └── page.tsx │ │ │ ├── callback │ │ │ │ ├── Callback.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── logout │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── remove-auth-and-login │ │ │ │ └── page.tsx │ │ │ └── token │ │ │ │ └── route.ts │ │ ├── page.tsx │ │ ├── setting │ │ │ ├── SettingClient │ │ │ │ ├── BtnList │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── UserInfo │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── account │ │ │ │ ├── AccountClient │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ └── page.tsx │ │ │ ├── data-control │ │ │ │ ├── DataControlClient │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── user-info │ │ │ │ ├── UserInfoClient │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ └── test │ │ │ ├── (component) │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ │ ├── inner │ │ │ ├── index.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── styles.ts │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── styles.ts │ ├── favicon.ico │ ├── layout.tsx │ └── page.tsx ├── components │ ├── BtnsBlock │ │ └── index.tsx │ ├── EmptyPage │ │ └── index.tsx │ ├── Loading │ │ └── index.tsx │ ├── ReturnBtn │ │ └── index.tsx │ └── Title │ │ └── index.tsx ├── config │ └── oidc.mjs ├── i18n.ts ├── layout │ ├── AppLayout │ │ ├── SideBar │ │ │ ├── Chats │ │ │ │ ├── Item.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── SideBarHeader │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── UserInfoBottom.tsx │ │ │ ├── index.draggable.panel.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── index.tsx │ ├── AppLayoutTemplate │ │ └── index.tsx │ ├── AppNoAuthLayout │ │ ├── SideBar │ │ │ ├── Chats │ │ │ │ ├── Item.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── SideBarHeader │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── UserInfoBottom.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── index.tsx │ ├── AppSkeletonLayout │ │ ├── SideBar │ │ │ ├── Chats │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── SideBarHeader │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ │ ├── UserInfoBottom.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── index.tsx │ ├── AuthLayout │ │ └── index.tsx │ ├── AxiosConfigLayout │ │ └── index.tsx │ ├── GlobalLayout │ │ ├── ThemeLayout │ │ │ └── index.tsx │ │ ├── globals.css │ │ └── index.tsx │ ├── PWAHandlerLayout │ │ └── index.tsx │ └── StyleRegistry.tsx ├── middleware.ts ├── store │ └── index.ts ├── styles │ ├── antdOverride.ts │ ├── global.ts │ ├── index.ts │ └── not-found-styles.ts ├── theme │ └── themeConfig.ts ├── types │ └── user.ts └── utils │ ├── axios.ts │ ├── client.ts │ ├── constants.ts │ └── index.ts └── tsconfig.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@yuntijs/lint').commitlint; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | *.out 4 | *.log 5 | mock 6 | .next 7 | *.dockerfile 8 | Dockerfile 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ############################################# 2 | ############ OIDC Server Configs ############ 3 | ############################################# 4 | 5 | # oidc server url, e.g. https://kubeagi.com/oidc 6 | OIDC_SERVER_URL=[oidc_server_url] 7 | 8 | # oidc server client id && secret 9 | CLIENT_ID=[client_id] 10 | CLIENT_SECRET=[client_secret] 11 | 12 | # bff-server origin, for SSR 13 | BFF_SERVER_ORIGIN=http://localhost:3000 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Eslintignore for YuntiJS 2 | ################################################################ 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # ci 8 | .coverage 9 | coverage 10 | 11 | # test 12 | jest* 13 | _test_ 14 | __test__ 15 | 16 | # umi 17 | .umi 18 | .umi-production 19 | .umi-test 20 | .dumi/tmp* 21 | 22 | # production 23 | dist 24 | es 25 | lib 26 | logs 27 | 28 | # misc 29 | # add other ignore file below 30 | 31 | # next 32 | # production 33 | /build 34 | 35 | # next.js 36 | /.next/ 37 | /out/ 38 | 39 | # vercel 40 | .vercel 41 | 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@yuntijs/lint').eslint; 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 反馈缺陷 Bug Report' 2 | description: '反馈一个问题缺陷 | Report an bug' 3 | title: '[Bug] ' 4 | labels: '🐛 Bug' 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '💻 系统环境 | Operating System' 9 | options: 10 | - Windows 11 | - macOS 12 | - Ubuntu 13 | - Other Linux 14 | - Other 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: '🐛 问题描述 | Bug Description' 20 | description: A clear and concise description of the bug. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: '🚦 期望结果 | Expected Behavior' 26 | description: A clear and concise description of what you expected to happen. 27 | - type: textarea 28 | attributes: 29 | label: '📷 复现步骤 | Recurrence Steps' 30 | description: A clear and concise description of how to recurrence. 31 | - type: textarea 32 | attributes: 33 | label: '📝 补充信息 | Additional Information' 34 | description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 功能需求 Feature Request' 2 | description: '需求或建议 | Suggest an idea' 3 | title: '[Request] ' 4 | labels: '🌠 Feature Request' 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 需求描述 | Feature Description' 9 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 解决方案 | Proposed Solution' 15 | description: Describe the solution you'd like in a clear and concise manner. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 补充信息 | Additional Information' 21 | description: Add any other context about the problem here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_question.yml: -------------------------------------------------------------------------------- 1 | name: '😇 疑问或帮助 Help Wanted' 2 | description: '疑问或需要帮助 | Need help' 3 | title: '[Question] ' 4 | labels: '😇 Help Wanted' 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🧐 问题描述 | Proposed Solution' 9 | description: A clear and concise description of the proplem. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '📝 补充信息 | Additional Information' 15 | description: Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4_other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📝 其他 Other' 3 | about: '其他问题 | Other issues' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - [ ] ✨ feat 6 | - [ ] 🐛 fix 7 | - [ ] 💄 style 8 | - [ ] 🔨 chore 9 | - [ ] 📝 docs 10 | 11 | #### 🔀 变更说明 | Description of Change 12 | 13 | 14 | 15 | #### 📝 补充信息 | Additional Information 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge 2 | on: 3 | pull_request_target: 4 | types: [labeled, edited] 5 | 6 | jobs: 7 | merge: 8 | if: contains(github.event.pull_request.labels.*.name, 'dependencies') 9 | name: Dependabot Auto Merge 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js v18.x 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18 18 | 19 | - name: Install pnpm v8.x 20 | uses: pnpm/action-setup@v2 21 | with: 22 | version: 8 23 | 24 | - name: Install deps 25 | run: pnpm i 26 | 27 | - name: Merge 28 | uses: ahmadnassri/action-dependabot-auto-merge@v2 29 | with: 30 | command: merge 31 | target: minor 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker image build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | env: 10 | REGISTRY: docker.io 11 | DIST_IMAGE_NAME: kubeagi/agent-portal-dist 12 | IMAGE_NAME: kubeagi/agent-portal 13 | DOCKER_USER: kubeagi 14 | 15 | jobs: 16 | image: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set Variable 23 | id: set-env 24 | run: | 25 | TAG=$(git describe --tags --abbrev=0 --match 'v*' 2> /dev/null) || true 26 | if [ -z "$TAG" ]; then 27 | echo "No tag found, use v0.1.0 as default" 28 | TAG=v0.1.0 29 | fi 30 | echo "TAG=${TAG}" >> $GITHUB_OUTPUT 31 | echo "DATE=$(TZ=Asia/Shanghai date +'%Y%m%d')" >> $GITHUB_OUTPUT 32 | - name: Show Variable 33 | run: echo "varibables ${{ steps.set-env.outputs.TAG }}-${{ steps.set-env.outputs.DATE }}" 34 | - name: Extract metadata (tags, labels) for Docker 35 | id: meta 36 | uses: docker/metadata-action@v5 37 | with: 38 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v3 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v3 43 | with: 44 | buildkitd-flags: --debug 45 | buildkitd-config-inline: | 46 | [worker.oci] 47 | max-parallelism = 1 48 | - name: Login to the dockerhub Registry 49 | uses: docker/login-action@v3 50 | with: 51 | username: ${{ env.DOCKER_USER }} 52 | password: ${{ secrets.DOCKER_TOKEN }} 53 | - uses: benjlevesque/short-sha@v3.0 54 | name: Get short commit sha 55 | id: short-sha 56 | - name: Build dist 57 | id: dist-build 58 | uses: docker/build-push-action@v5 59 | with: 60 | context: . 61 | file: build.dockerfile 62 | platforms: linux/amd64 63 | tags: | 64 | ${{ env.REGISTRY }}/${{ env.DIST_IMAGE_NAME }}:main 65 | push: true 66 | build-args: | 67 | GITHUB_SHA=${{ github.sha }} 68 | - name: Build and push image 69 | id: build-push 70 | uses: docker/build-push-action@v5 71 | with: 72 | context: . 73 | file: Dockerfile 74 | platforms: linux/amd64,linux/arm64 75 | tags: | 76 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 77 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.set-env.outputs.TAG }}-${{ steps.set-env.outputs.DATE }}-${{ steps.short-sha.outputs.sha }} 78 | ${{ steps.meta.outputs.tags }} 79 | push: true 80 | -------------------------------------------------------------------------------- /.github/workflows/issue-auto-comments.yml: -------------------------------------------------------------------------------- 1 | name: Issue Auto Comment 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - closed 8 | - assigned 9 | pull_request_target: 10 | types: 11 | - opened 12 | - closed 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | run: 19 | permissions: 20 | issues: write # for actions-cool/issues-helper to update issues 21 | pull-requests: write # for actions-cool/issues-helper to update PRs 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Auto Comment on Issues Opened 25 | uses: wow-actions/auto-comment@v1 26 | with: 27 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 28 | issuesOpened: | 29 | 👀 @{{ author }} 30 | Thank you for raising an issue. We will investigate into the matter and get back to you as soon as possible. 31 | Please make sure you have given us as much context as possible.\ 32 | 非常感谢您提交 issue。我们会尽快调查此事,并尽快回复您。 请确保您已经提供了尽可能多的背景信息。 33 | - name: Auto Comment on Issues Closed 34 | uses: wow-actions/auto-comment@v1 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 37 | issuesClosed: | 38 | ✅ @{{ author }} 39 | 40 | This issue is closed, If you have any questions, you can comment and reply.\ 41 | 此问题已经关闭。如果您有任何问题,可以留言并回复。 42 | - name: Auto Comment on Pull Request Opened 43 | uses: wow-actions/auto-comment@v1 44 | with: 45 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN}} 46 | pullRequestOpened: | 47 | 👍 @{{ author }} 48 | 49 | Thank you for raising your pull request and contributing to our Community 50 | Please make sure you have followed our contributing guidelines. We will review it as soon as possible. 51 | If you encounter any problems, please feel free to connect with us.\ 52 | 非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。 53 | 如果您遇到任何问题,请随时与我们联系。 54 | - name: Auto Comment on Pull Request Merged 55 | uses: actions-cool/pr-welcome@main 56 | if: github.event.pull_request.merged == true 57 | with: 58 | token: ${{ secrets.GH_TOKEN }} 59 | comment: | 60 | ❤️ Great PR @${{ github.event.pull_request.user.login }} ❤️ 61 | 62 | The growth of project is inseparable from user feedback and contribution, thanks for your contribution!\ 63 | 项目的成长离不开用户反馈和贡献,感谢您的贡献! 64 | emoji: 'hooray' 65 | pr-emoji: '+1, heart' 66 | - name: Remove inactive 67 | if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login 68 | uses: actions-cool/issues-helper@v3 69 | with: 70 | actions: 'remove-labels' 71 | token: ${{ secrets.GH_TOKEN }} 72 | issue-number: ${{ github.event.issue.number }} 73 | labels: 'Inactive' 74 | -------------------------------------------------------------------------------- /.github/workflows/issue-check-inactive.yml: -------------------------------------------------------------------------------- 1 | name: Issue Check Inactive 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 */15 * *' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | issue-check-inactive: 12 | permissions: 13 | issues: write # for actions-cool/issues-helper to update issues 14 | pull-requests: write # for actions-cool/issues-helper to update PRs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: check-inactive 18 | uses: actions-cool/issues-helper@v3 19 | with: 20 | actions: 'check-inactive' 21 | inactive-label: 'Inactive' 22 | inactive-day: 30 23 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-require.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Require 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | issue-close-require: 12 | permissions: 13 | issues: write # for actions-cool/issues-helper to update issues 14 | pull-requests: write # for actions-cool/issues-helper to update PRs 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: need reproduce 18 | uses: actions-cool/issues-helper@v3 19 | with: 20 | actions: 'close-issues' 21 | labels: '✅ Fixed' 22 | inactive-day: 3 23 | body: | 24 | Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. 25 | 26 | 由于该 issue 被标记为已修复,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 27 | - name: need reproduce 28 | uses: actions-cool/issues-helper@v3 29 | with: 30 | actions: 'close-issues' 31 | labels: '🤔 Need Reproduce' 32 | inactive-day: 3 33 | body: | 34 | Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. 35 | 36 | 由于该 issue 被标记为需要更多信息,却 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 37 | - name: need reproduce 38 | uses: actions-cool/issues-helper@v3 39 | with: 40 | actions: 'close-issues' 41 | labels: "🙅🏻♀️ WON'T DO" 42 | inactive-day: 3 43 | body: | 44 | Since the issue was labeled with `🙅🏻♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply. 45 | 46 | 由于该 issue 被标记为暂不处理,同时 3 天未收到回应。现关闭 issue,若有任何问题,可评论回复。 47 | -------------------------------------------------------------------------------- /.github/workflows/issue-remove-inactive.yml: -------------------------------------------------------------------------------- 1 | name: Issue Remove Inactive 2 | 3 | on: 4 | issues: 5 | types: [edited] 6 | issue_comment: 7 | types: [created, edited] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | issue-remove-inactive: 14 | permissions: 15 | issues: write # for actions-cool/issues-helper to update issues 16 | pull-requests: write # for actions-cool/issues-helper to update PRs 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: remove inactive 20 | if: github.event.issue.state == 'open' && github.actor == github.event.issue.user.login 21 | uses: actions-cool/issues-helper@v3 22 | with: 23 | actions: 'remove-labels' 24 | issue-number: ${{ github.event.issue.number }} 25 | labels: 'Inactive' 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - '!master' 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js v18.x 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18 18 | 19 | - name: Install pnpm v8.x 20 | uses: pnpm/action-setup@v2 21 | with: 22 | version: 8 23 | 24 | - name: Install deps 25 | run: pnpm install 26 | # run: pnpm install --only=dev --ignore-scripts && pnpm add react -D --only=dev --ignore-scripts 27 | 28 | - name: Lint 29 | run: npm run lint 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore for YuntiJS 2 | ################################################################ 3 | 4 | # general 5 | .DS_Store 6 | .idea 7 | .vscode 8 | .history 9 | .temp 10 | .env.local 11 | venv 12 | temp 13 | tmp 14 | 15 | # dependencies 16 | node_modules 17 | *.log 18 | *.lock 19 | package-lock.json 20 | bun.lockb 21 | 22 | # ci 23 | .coverage 24 | .eslintcache 25 | .stylelintcache 26 | coverage 27 | 28 | # production 29 | dist 30 | es 31 | lib 32 | umd 33 | logs 34 | test-output 35 | 36 | # husky 37 | .husky/prepare-commit-msg 38 | 39 | # dependencies 40 | /.pnp 41 | .pnp.js 42 | .yarn/install-state.gz 43 | 44 | # next.js 45 | /.next/ 46 | /out/ 47 | 48 | # production 49 | /build 50 | 51 | # dev 52 | .env.development 53 | .env.production 54 | 55 | # misc 56 | *.pem 57 | /cert/*.crt 58 | /cert/*.key 59 | 60 | # local env files 61 | .env*.local 62 | 63 | # vercel 64 | .vercel 65 | 66 | # typescript 67 | *.tsbuildinfo 68 | next-env.d.ts 69 | 70 | # misc 71 | # add other ignore file below 72 | public/*.js 73 | public/*.js.map 74 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | public-hoist-pattern[]=*@umijs/lint* 3 | public-hoist-pattern[]=*changelog* 4 | public-hoist-pattern[]=*commitlint* 5 | public-hoist-pattern[]=*eslint* 6 | public-hoist-pattern[]=*postcss* 7 | public-hoist-pattern[]=*prettier* 8 | public-hoist-pattern[]=*remark* 9 | public-hoist-pattern[]=*semantic-release* 10 | public-hoist-pattern[]=*stylelint* 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettierignore for YuntiJS 2 | ################################################################ 3 | 4 | # general 5 | .DS_Store 6 | .editorconfig 7 | .idea 8 | .vscode 9 | .history 10 | .temp 11 | .env.local 12 | .husky 13 | .npmrc 14 | venv 15 | temp 16 | tmp 17 | LICENSE 18 | 19 | # dependencies 20 | node_modules 21 | *.log 22 | *.lock 23 | package-lock.json 24 | 25 | # ci 26 | .coverage 27 | .eslintcache 28 | .stylelintcache 29 | coverage 30 | test-output 31 | 32 | # production 33 | dist 34 | es 35 | lib 36 | logs 37 | 38 | # umi 39 | .umi 40 | .umi-production 41 | .umi-test 42 | .dumi/tmp* 43 | 44 | # ignore files 45 | .*ignore 46 | 47 | # docker 48 | docker 49 | Dockerfile* 50 | 51 | # image 52 | *.webp 53 | *.gif 54 | *.png 55 | *.jpg 56 | 57 | # misc 58 | # add other ignore file below 59 | 60 | # next 61 | # production 62 | /build 63 | 64 | # next.js 65 | /.next/ 66 | /out/ 67 | 68 | # vercel 69 | .vercel 70 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@yuntijs/lint').prettier; 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /public 2 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@yuntijs/lint').stylelint; 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 kubeagi/agent-portal-dist:main as dist 2 | 3 | # dockerfile of base image 4 | FROM node:20-alpine 5 | 6 | # Create app directory 7 | RUN mkdir -p /usr/src/app 8 | WORKDIR /usr/src/app 9 | 10 | COPY --from=dist /build-files /usr/src/app 11 | 12 | ENV NODE_ENV=production 13 | 14 | # Install dependencies modules 15 | ADD .npmrc package.json pnpm-lock.yaml ./ 16 | RUN npm i pnpm -g 17 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ 18 | pnpm install --prod --frozen-lockfile --ignore-scripts 19 | 20 | EXPOSE 3000 21 | 22 | CMD ["node", "server.mjs"] 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Console to let user manage their AI agents for their preference. 2 | 3 | - Add existing agent 4 | - Build your own AI agent 5 | - Chat with agents 6 | - Keep chat history 7 | - .. 8 | 9 | ## Environment 10 | 11 | - node >= v18.17.0 12 | 13 | ## Getting Started 14 | 15 | First, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | yarn dev 21 | # or 22 | pnpm dev 23 | # or 24 | bun dev 25 | ``` 26 | 27 | Open with your browser to see the result. 28 | 29 | ## .env 配置 (示例: ./.env.example) 30 | 31 | ### 开发模式 32 | 33 | ``` 34 | cp .env.example .env.development 35 | ``` 36 | 37 | 复制并重命名为 .env.development, 修改 oidc(必须) 等参数 38 | 39 | ### 生产模式 40 | 41 | ``` 42 | cp .env.example .env.production 43 | ``` 44 | 45 | 复制并重命名为 .env.production, 修改 oidc(必须) 等参数 46 | 47 | ## 本地运行 pwa (添加到桌面) 48 | 49 | - 构建 50 | 51 | ``` 52 | npm run build 53 | ``` 54 | 55 | - 安装 [mkcert](https://github.com/FiloSottile/mkcert) 并生成证书 56 | 57 | ``` 58 | mkcert -install 59 | mkcert localhost 60 | ``` 61 | 62 | - 运行带自签名证书的生产模式 63 | 64 | ``` 65 | npm run start:https 66 | ``` 67 | -------------------------------------------------------------------------------- /build.dockerfile: -------------------------------------------------------------------------------- 1 | # dockerfile of base image 2 | FROM node:20-alpine as builder 3 | 4 | # Create app directory 5 | RUN mkdir -p /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | ARG GITHUB_SHA 9 | ENV GITHUB_SHA=$GITHUB_SHA 10 | 11 | # Install dependencies modules 12 | ADD .npmrc package.json pnpm-lock.yaml ./ 13 | 14 | RUN npm i pnpm -g 15 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ 16 | pnpm install --frozen-lockfile --ignore-scripts 17 | 18 | # Build portal 19 | ADD . ./ 20 | RUN npm run build && \ 21 | rm -rf .next/cache && \ 22 | mkdir -p /tmp/dist && \ 23 | mv public .next server.mjs package.json -t /tmp/dist/ 24 | 25 | # Save dist 26 | FROM kubeagi/busybox 27 | COPY --from=builder /tmp/dist /build-files 28 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | image="kubeagi/agent-portal" 5 | 6 | # 1.构建静态文件镜像 7 | docker build -t kubeagi/agent-portal-dist:main -f build.dockerfile --secret id=npmrc,src=$HOME/.npmrc . 8 | 9 | # 2.将静态文件打包到镜像中 10 | docker build -t $image --secret id=npmrc,src=$HOME/.npmrc . 11 | 12 | docker push $image 13 | -------------------------------------------------------------------------------- /kiwi-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "baiduApiKey": { 3 | "appId": "20230227001578444", 4 | "appKey": "U1bG31W0HhRhWD8j6Y9T" 5 | }, 6 | "baiduLangMap": { 7 | "en-US": "en" 8 | }, 9 | "defaultTranslateKeyApi": "Pinyin", 10 | "distLangs": ["en"], 11 | "googleApiKey": "", 12 | "ignoreDir": ["./.next", "./src/app/[locale]/test/*"], 13 | "ignoreFile": [], 14 | "importI18N": "// todo remove, useTranslations from next-intl", 15 | "kiwiDir": "./messages", 16 | "srcLang": "zh", 17 | "translateOptions": { 18 | "concurrentLimit": 10, 19 | "requestOptions": {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /messages/en/AccountClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | tongGuoNiDePian: 4 | 'Based on your preference features, we recommend content that may be of interest to you.', 5 | zhangHaoSheZhi: 'Account Setting', 6 | zhuXiaoZhangHu: 'Logout', 7 | youXiang: 'Email', 8 | weiXin: 'WeChat', 9 | shouJi: 'Mobile', 10 | geXingHuaNeiRong: 'Personalized content recommendation', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /messages/en/BtnList.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | tuiChuDengLu: 'Logout', 4 | guanYu: 'About', 5 | shiYongFanKui: 'Feedback', 6 | tianJiaZhiZhuoMian: 'Add to Desktop', 7 | fenXiang: 'Share', 8 | woDeZhiNengTi: 'My AI Agents', 9 | geRenZiLiao: 'Profile', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /messages/en/BtnsBlock.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | moRenBiaoTi: 'Default Title', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/en/Chats.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaBaLaBa: 'Conversation blah blah...', 4 | moRenDuiHua: 'Default conversation', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/en/Conversation.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaKuang: 'Dialogue Box', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/en/DataControlClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHua: 'Conversation 1', 4 | shanChu: 'Delete', 5 | caoZuo: 'Operation', 6 | fenXiangShiJian: 'Shared Time', 7 | duiHuaMingCheng: 'Conversation Name', 8 | fenXiangLianJie: 'Share Link', 9 | jiangCiLiuLanQi: 10 | 'Save new chat logs from this browser to your history and allow us to utilize your chat history to improve our models. Turning off the switch will not retain your chat logs. This setting does not sync across browsers or devices.', 11 | shuJuKongZhi: 'Data Control', 12 | shanChuSuoYouLiao: 'Delete all chat history', 13 | liaoTianJiLuYing: 'Chat History Application', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /messages/en/SideBar.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaLieBiao: 'Conversation List', 4 | zhiNengTiLieBiao: 'AI Agent List', 5 | }, 6 | UserInfoBottom: { 7 | sheZhi: 'Settings', 8 | genSuiXiTong: 'Follow System', 9 | heiAnMoShi: 'Dark Mode', 10 | liangSeMoShi: 'Light Mode', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /messages/en/SideBarHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | xinDuiHua: 'New Conversation', 4 | duiHua: 'Conversation', 5 | faXianZhiNengTi: 'Discover AI Agent', 6 | chuangJianZhiNengTi: 'Create AI Agent', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /messages/en/UserInfoClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | wanCheng: 'Complete', 4 | qingShuRuZhangHao: 'Input account', 5 | zhangHao: 'Account', 6 | zhiNengShiYongZi: 7 | 'Only letters, numbers, and underscores are allowed, with a length of 4-16 characters', 8 | qingShuRuNiCheng: 'Input nickname', 9 | niCheng: 'Nickname', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /messages/en/app.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | not_found: { 3 | fanHuiShouYe: 'Return to home page', 4 | henBaoQianYeMian: 'Sorry, the page seems to have gotten lost。', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/en/callback.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | Callback: { 3 | renZhengShiBaiQing: 'Authentication failed, please try again', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/en/components.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | faXianAIZhi: 'Discover Agents', 4 | dengLu: 'Login', 5 | tuiJian: 'Recommend', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /messages/en/create.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | page: { 3 | yuLan: 'Preview', 4 | shangChuanWenJian: 'Upload', 5 | qingShangChuanZhiShi: 'Upload knowledge base file', 6 | zhiShiKu: 'Knowledge Base', 7 | qingShuRuAI: 'Input description of AI Agent', 8 | zhiNengTiMiaoShu: 'Description of AI Agent', 9 | qingXuanZeAI: 'Select category of AI Agent', 10 | zhiNengTiFenLei: 'Category of AI Agent', 11 | qingShuRuAI2: 12 | 'Input the role setting for the AI Agent, such as its purpose and capabilities. What is the role of this agent? What can it do? What are the considerations? \r\n Example: \r\n You are an AI product expert who is familiar with the entire lifecycle of large AI models. You have extensive knowledge of developing, training, evaluating, fine-tuning, deploying, and using large models. You are well-versed in all AI-related concepts. You can assist users in formulating design concepts, improving design ideas, identifying design issues, and more. However, you are a meticulous expert and will not provide fabricated answers.', 13 | qingShuRuAI3: 'Input role setting of AI Agent ', 14 | sheDing: 'Settings', 15 | qingShuRuAI4: 'Input name of AI Agent', 16 | zhiNengTiMingCheng: 'Name of AI Agent', 17 | shangChuanTouXiang: 'Upload avatar', 18 | chuangJianAIZhi: 'Create AI Agent', 19 | baoCun: 'Save', 20 | gongKaiSuoYouRen: 'Public · Everyone can engage in conversation', 21 | tongGuoLianJieFang: 'Access via Link · Users with the link can engage in conversation', 22 | siMiJinZiJi: 'Private · Only yourself can engage in conversation', 23 | qiTa: 'Others', 24 | shengHuoQuWei: 'Life Enjoyment', 25 | jueSeBanYan: 'Role Play', 26 | yingYinShengCheng: 'Video Generation', 27 | aIHuiHua: 'AI Painting', 28 | neiRongChuangZuo: 'Content Creation', 29 | gongZuoXueXi: 'Work & Learning', 30 | tongYongDuiHua: 'General Conversation', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /messages/en/index.ts: -------------------------------------------------------------------------------- 1 | import AccountClient from './AccountClient'; 2 | import BtnList from './BtnList'; 3 | import BtnsBlock from './BtnsBlock'; 4 | import Chats from './Chats'; 5 | import Conversation from './Conversation'; 6 | import DataControlClient from './DataControlClient'; 7 | import SideBar from './SideBar'; 8 | import SideBarHeader from './SideBarHeader'; 9 | import UserInfoClient from './UserInfoClient'; 10 | import app from './app'; 11 | import callback from './callback'; 12 | import components from './components'; 13 | import create from './create'; 14 | import oidc from './oidc'; 15 | import setting from './setting'; 16 | import utils from './utils'; 17 | 18 | export default Object.assign( 19 | {}, 20 | { 21 | components, 22 | create, 23 | Conversation, 24 | app, 25 | callback, 26 | oidc, 27 | AccountClient, 28 | DataControlClient, 29 | setting, 30 | BtnList, 31 | UserInfoClient, 32 | BtnsBlock, 33 | Chats, 34 | SideBar, 35 | SideBarHeader, 36 | utils, 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /messages/en/oidc.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | layout: { 3 | jiaZaiZhong: 'Loading ...', 4 | dengChuZhong: 'Logging out...', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/en/setting.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | page: { 3 | geRenSheZhi: 'Personal Settings', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/en/utils.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | constants: { 3 | aIHuiHua: 'AI Painting', 4 | youXiDongMan: 'Games & Anime', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/zh/AccountClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | tongGuoNiDePian: '通过你的偏好特征,向你推荐可能感兴趣的内容。', 4 | zhangHaoSheZhi: '账号设置', 5 | zhuXiaoZhangHu: '注销账户', 6 | youXiang: '邮箱', 7 | weiXin: '微信', 8 | shouJi: '手机', 9 | geXingHuaNeiRong: '个性化内容推荐', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /messages/zh/BtnList.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | tuiChuDengLu: '退出登录', 4 | guanYu: '关于', 5 | shiYongFanKui: '使用反馈', 6 | tianJiaZhiZhuoMian: '添加至桌面', 7 | fenXiang: '分享', 8 | woDeZhiNengTi: '我的智能体', 9 | geRenZiLiao: '个人资料', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /messages/zh/BtnsBlock.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | moRenBiaoTi: '默认标题', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/zh/Chats.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaBaLaBa: '对话巴拉巴拉....', 4 | moRenDuiHua: '默认对话', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/zh/Conversation.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaKuang: '对话框', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/zh/DataControlClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHua: '对话1', 4 | shanChu: '删除', 5 | caoZuo: '操作', 6 | fenXiangShiJian: '分享时间', 7 | duiHuaMingCheng: '对话名称', 8 | fenXiangLianJie: '分享链接', 9 | jiangCiLiuLanQi: 10 | '将此浏览器上的新聊天记录保存到您的历史记录中,并允许我们应用您的聊天记录,改进我们的模型。关闭开关,将不会保留您的聊天记录。此设置不在浏览器或设备之间同步。', 11 | shuJuKongZhi: '数据控制', 12 | shanChuSuoYouLiao: '删除所有聊天记录', 13 | liaoTianJiLuYing: '聊天记录应用', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /messages/zh/SideBar.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | duiHuaLieBiao: '对话列表', 4 | zhiNengTiLieBiao: '智能体列表', 5 | }, 6 | UserInfoBottom: { 7 | sheZhi: '设置', 8 | genSuiXiTong: '跟随系统', 9 | heiAnMoShi: '黑暗模式', 10 | liangSeMoShi: '亮色模式', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /messages/zh/SideBarHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | xinDuiHua: '新对话', 4 | duiHua: '对话', 5 | faXianZhiNengTi: '发现智能体', 6 | chuangJianZhiNengTi: '创建智能体', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /messages/zh/UserInfoClient.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | wanCheng: '完成', 4 | qingShuRuZhangHao: '请输入账号', 5 | zhangHao: '账号', 6 | zhiNengShiYongZi: '只能使用字母、数字以及下划线,长度为 4-16 个字符', 7 | qingShuRuNiCheng: '请输入昵称', 8 | niCheng: '昵称', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /messages/zh/app.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | not_found: { 3 | fanHuiShouYe: '返回首页', 4 | henBaoQianYeMian: '很抱歉,页面不小心迷路了', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/zh/callback.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | Callback: { 3 | renZhengShiBaiQing: '认证失败请重试', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/zh/components.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | faXianAIZhi: '发现智能体', 4 | dengLu: '登录', 5 | tuiJian: '推荐', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /messages/zh/create.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | page: { 3 | yuLan: '预览', 4 | shangChuanWenJian: '上传文件', 5 | qingShangChuanZhiShi: '请上传知识库文件', 6 | zhiShiKu: '知识库', 7 | qingShuRuAI: '请输入 AI 智能体描述', 8 | zhiNengTiMiaoShu: '智能体描述', 9 | qingXuanZeAI: '请选择 AI 智能体分类', 10 | zhiNengTiFenLei: '智能体分类', 11 | qingShuRuAI2: 12 | '请输入 AI 智能体角色设定,如这个智能体有什么作用?他能做什么事情?它应该有哪些注意事项?\r\n示例:\r\n你是一个 AI 产品专家,你熟悉 AI 大模型的全生命周期,对于大模型的开发、训练、评估、调优、部署以及使用等你都非常了解,你知晓所有 AI 相关的概念。你可以帮助用户提出设计理念、完善设计思路、指出设计问题等,但是你是一个严谨的专家,你不会编造回答。', 13 | qingShuRuAI3: '请输入 AI 智能体角色设定', 14 | sheDing: '设定', 15 | qingShuRuAI4: '请输入 AI 智能体名称', 16 | zhiNengTiMingCheng: '智能体名称', 17 | shangChuanTouXiang: '上传头像', 18 | chuangJianAIZhi: '创建 AI 智能体', 19 | baoCun: '保存', 20 | gongKaiSuoYouRen: '公开 · 所有人可对话', 21 | tongGuoLianJieFang: '通过链接访问 · 获得链接的用户可对话', 22 | siMiJinZiJi: '私密 · 仅自己可对话', 23 | qiTa: '其他', 24 | shengHuoQuWei: '生活趣味', 25 | jueSeBanYan: '角色扮演', 26 | yingYinShengCheng: '影音生成', 27 | aIHuiHua: 'AI 绘画', 28 | neiRongChuangZuo: '内容创作', 29 | gongZuoXueXi: '工作学习', 30 | tongYongDuiHua: '通用对话', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /messages/zh/index.ts: -------------------------------------------------------------------------------- 1 | import AccountClient from './AccountClient'; 2 | import BtnList from './BtnList'; 3 | import BtnsBlock from './BtnsBlock'; 4 | import Chats from './Chats'; 5 | import Conversation from './Conversation'; 6 | import DataControlClient from './DataControlClient'; 7 | import SideBar from './SideBar'; 8 | import SideBarHeader from './SideBarHeader'; 9 | import UserInfoClient from './UserInfoClient'; 10 | import app from './app'; 11 | import callback from './callback'; 12 | import components from './components'; 13 | import create from './create'; 14 | import oidc from './oidc'; 15 | import setting from './setting'; 16 | import utils from './utils'; 17 | 18 | export default Object.assign( 19 | {}, 20 | { 21 | components, 22 | create, 23 | Conversation, 24 | app, 25 | callback, 26 | oidc, 27 | AccountClient, 28 | DataControlClient, 29 | setting, 30 | BtnList, 31 | UserInfoClient, 32 | BtnsBlock, 33 | Chats, 34 | SideBar, 35 | SideBarHeader, 36 | utils, 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /messages/zh/oidc.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | layout: { 3 | jiaZaiZhong: '加载中...', 4 | dengChuZhong: '登出中...', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /messages/zh/setting.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | page: { 3 | geRenSheZhi: '个人设置', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /messages/zh/utils.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | constants: { 3 | aIHuiHua: 'AI 绘画', 4 | youXiDongMan: '游戏动漫', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextPWA from '@ducanh2912/next-pwa'; 2 | import analyzer from '@next/bundle-analyzer'; 3 | import { execSync } from 'child_process'; 4 | import webpack from 'webpack'; 5 | import createNextIntlPlugin from 'next-intl/plugin'; 6 | 7 | const withNextIntl = createNextIntlPlugin(); 8 | 9 | const getLastCommitHash = () => { 10 | try { 11 | return execSync('git rev-parse HEAD').toString().trim(); 12 | } catch (error) { 13 | console.warn('Get last commit hash faild =>', error); 14 | return '-'; 15 | } 16 | }; 17 | 18 | const isProd = process.env.NODE_ENV === 'production'; 19 | 20 | const withBundleAnalyzer = analyzer({ 21 | enabled: process.env.ANALYZE === 'true', 22 | }); 23 | 24 | const withPWA = nextPWA({ 25 | dest: 'public', 26 | register: true, 27 | workboxOptions: { 28 | skipWaiting: true, 29 | }, 30 | }); 31 | 32 | 33 | /** 34 | * Licensed Materials 35 | * (C) Copyright 2024 KubeAGI. All Rights Reserved. 36 | * @date 1702870032801 37 | * @hash 72dd15d6d9b660cb4f7b47c2374332bf10afc7e7 38 | */ 39 | 40 | // const site = 'k8s.com.cn'; 41 | const bannerFlag = 'Licensed Materials'; // `Licensed Materials - Property of ${site}`; 42 | const banner = `${bannerFlag} 43 | (C) Copyright 2024 KubeAGI. All Rights Reserved. 44 | @date ${Date.now()} 45 | @hash ${process.env.GITHUB_SHA || getLastCommitHash()}`; 46 | 47 | const nextConfig = { 48 | compress: isProd, 49 | typescript: { 50 | ignoreBuildErrors: true, 51 | }, 52 | // productionBrowserSourceMaps: true, 53 | experimental: { 54 | forceSwcTransforms: true, 55 | // turbo: { // dev with turbo 56 | // rules: { 57 | // '*.svg': { 58 | // loaders: ['@svgr/webpack'], 59 | // as: '*.js', 60 | // }, 61 | // }, 62 | // }, 63 | webVitalsAttribution: ['CLS', 'LCP'] 64 | }, 65 | images: { 66 | unoptimized: !isProd, 67 | }, 68 | reactStrictMode: isProd, 69 | transpilePackages: ['antd', '@ant-design', 'antd-style', '@lobehub/ui', 'antd-mobile'], 70 | webpack: (config, { isServer }) => { 71 | config.experiments = { 72 | asyncWebAssembly: true, 73 | layers: true, 74 | }; 75 | 76 | // to fix shikiji compile error 77 | // refs: https://github.com/antfu/shikiji/issues/23 78 | config.module.rules.push({ 79 | test: /\.m?js$/, 80 | type: 'javascript/auto', 81 | resolve: { 82 | fullySpecified: false, 83 | }, 84 | }); 85 | 86 | config.module.rules.push({ 87 | test: /\.svg$/i, 88 | use: ['@svgr/webpack'], 89 | }); 90 | if (!isServer) { // client side 91 | config.plugins.push(new webpack.BannerPlugin({ 92 | banner, 93 | exclude: /\.svg$/, 94 | })); 95 | } 96 | 97 | return config; 98 | }, 99 | } 100 | 101 | export default isProd ? 102 | withBundleAnalyzer(withPWA(withNextIntl(nextConfig))) 103 | : 104 | withNextIntl(nextConfig); 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-portal", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "next build", 6 | "build:analyze": "cross-env ANALYZE=true next build", 7 | "dev": "node server.mjs", 8 | "lint": "npm run lint:es && npm run lint:style", 9 | "lint-fix": "npm run lint-fix:es && npm run lint-fix:style", 10 | "lint-fix:es": "eslint --ext .jsx,.js,.tsx,.ts src --fix", 11 | "lint-fix:style": "stylelint \"{src,tests}/**/*.{css,less,js,jsx,ts,tsx}\" --fix", 12 | "lint:es": "eslint --ext .jsx,.js,.tsx,.ts src", 13 | "lint:style": "stylelint \"{src,tests}/**/*.{css,less,js,jsx,ts,tsx}\"", 14 | "prepare": "husky install", 15 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 16 | "start": "cross-env NODE_ENV=production node server.mjs", 17 | "start:https": "cross-env NODE_ENV=production PWA=true node server.mjs" 18 | }, 19 | "lint-staged": { 20 | "*.md": [ 21 | "remark --quiet --output --", 22 | "prettier --write --no-error-on-unmatched-pattern" 23 | ], 24 | "*.json": [ 25 | "prettier --write --no-error-on-unmatched-pattern" 26 | ], 27 | "*.{css,less}": [ 28 | "stylelint --fix --allow-empty-input", 29 | "prettier --write" 30 | ], 31 | "*.{js,jsx}": [ 32 | "stylelint --fix --allow-empty-input", 33 | "eslint --fix", 34 | "prettier --write" 35 | ], 36 | "*.{ts,tsx}": [ 37 | "stylelint --fix", 38 | "eslint --fix", 39 | "prettier --parser=typescript --write" 40 | ] 41 | }, 42 | "dependencies": { 43 | "@ant-design/icons": "^5.2.6", 44 | "@ant-design/nextjs-registry": "^1.0.0", 45 | "@lobehub/ui": "^1.125.8", 46 | "@next/mdx": "^14.0.4", 47 | "@reduxjs/toolkit": "^2.0.1", 48 | "@svgr/webpack": "^8.1.0", 49 | "@yuntijs/arcadia-bff-sdk": "^1.2.7", 50 | "@yuntijs/bff-client": "^0.3.3", 51 | "@yuntijs/chat": "0.1.0-beta.13", 52 | "antd": "^5.12.6", 53 | "antd-mobile": "^5.34.0", 54 | "antd-style": "^3.6.1", 55 | "axios": "^1.6.7", 56 | "axios-hooks": "^5.0.2", 57 | "babel-runtime": "^6.26.0", 58 | "classnames": "^2.5.1", 59 | "express": "^4.18.2", 60 | "http-proxy-middleware": "^2.0.6", 61 | "lodash": "^4.17.21", 62 | "lucide-react": "^0.304.0", 63 | "next": "^14.1.0", 64 | "next-intl": "^3.9.1", 65 | "nuqs": "^1.17.1", 66 | "query-string": "^8.1.0", 67 | "react": "^18", 68 | "react-dom": "^18", 69 | "react-infinite-scroll-component": "^6.1.0", 70 | "react-layout-kit": "^1.7.4", 71 | "react-redux": "^9.0.4", 72 | "redux": "^5.0.1", 73 | "sharp": "^0.33.2", 74 | "swr": "^2.2.4", 75 | "ua-parser-js": "2.0.0-alpha.2" 76 | }, 77 | "devDependencies": { 78 | "@ducanh2912/next-pwa": "^10.2.2", 79 | "@next/bundle-analyzer": "^14.1.0", 80 | "@types/lodash": "^4.14.202", 81 | "@types/mdx": "^2.0.10", 82 | "@types/node": "^20", 83 | "@types/query-string": "^6.3.0", 84 | "@types/react": "^18", 85 | "@types/react-dom": "^18", 86 | "@types/ua-parser-js": "^0.7.39", 87 | "@yuntijs/lint": "^1.4.0", 88 | "commitlint": "^18.4.3", 89 | "cross-env": "^7.0.3", 90 | "eslint": "^8.56.0", 91 | "eslint-config-next": "14.0.4", 92 | "husky": "^8.0.3", 93 | "lint-staged": "^15.2.0", 94 | "prettier": "^3.1.1", 95 | "remark": "^15.0.1", 96 | "remark-cli": "^12.0.0", 97 | "stylelint": "^15", 98 | "tslib": "^2.6.2", 99 | "typescript": "^5", 100 | "webpack": "^5.89.0" 101 | }, 102 | "packageManager": "pnpm@8.12.1", 103 | "engines": { 104 | "node": ">=18.17.0", 105 | "pnpm": ">=8.12.0" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /public/404/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/404/404.jpg -------------------------------------------------------------------------------- /public/404/4xx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/404/4xx.jpg -------------------------------------------------------------------------------- /public/create_gpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/create_gpt.png -------------------------------------------------------------------------------- /public/default_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/default_chat.png -------------------------------------------------------------------------------- /public/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/discover.png -------------------------------------------------------------------------------- /public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/icons/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background_color": "#FFFFFF", 3 | "display": "standalone", 4 | "icons": [ 5 | { 6 | "src": "/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "/icons/android-chrome-384x384.png", 13 | "sizes": "384x384", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icons/icon-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ], 22 | "name": "AgileGPT", 23 | "orientation": "portrait", 24 | "short_name": "AgileGPT App", 25 | "start_url": "/", 26 | "theme_color": "#FFFFFF" 27 | } 28 | -------------------------------------------------------------------------------- /public/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 16 | 17 | 21 | 25 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /server.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import https from 'https'; 3 | import next from 'next'; 4 | import express from 'express'; 5 | import { createProxyMiddleware } from 'http-proxy-middleware'; 6 | import url from 'url'; 7 | 8 | async function bootstrap() { 9 | const port = Number.parseInt(process.env.PORT, 10) || 3000; 10 | const dev = process.env.NODE_ENV !== 'production'; 11 | const app = next({ 12 | dev, 13 | customServer: true, 14 | }); 15 | const handle = app.getRequestHandler(); 16 | 17 | await app.prepare(); 18 | 19 | const oidcServerUrl = process.env.OIDC_SERVER_URL; 20 | if (!oidcServerUrl) { 21 | console.warn('The env OIDC_SERVER_URL must be configured!') 22 | process.exit(); 23 | } 24 | const oidcUrlObj = url.parse(oidcServerUrl); 25 | const _url = `${oidcUrlObj.protocol}//${oidcUrlObj.host}`; 26 | const server = express(); 27 | 28 | // 代理中间件配置 29 | const bff = '/bff'; 30 | const api = '/kubeagi-apis'; 31 | server.use(bff, createProxyMiddleware({ 32 | target: _url , 33 | changeOrigin: true, 34 | secure: false, // 关闭 SSL 证书验证 35 | })); 36 | server.use(api, createProxyMiddleware({ 37 | target: _url, 38 | changeOrigin: true, 39 | secure: false, 40 | })); 41 | 42 | // 处理其他所有请求 43 | server.all('*', (req, res) => { 44 | return handle(req, res); 45 | }); 46 | 47 | // PWA 测试时, 使用自签名证书, 开启HTTPS 48 | if (process.env.PWA === 'true') { 49 | // 在非开发环境下使用 HTTPS 50 | https.createServer({ 51 | key: fs.readFileSync('./localhost-key.pem'), 52 | cert: fs.readFileSync('./localhost.pem'), 53 | }, server).listen(port, err => { 54 | if (err) throw err; 55 | // eslint-disable-next-line no-console 56 | console.log(`> Ready on https://localhost:${port}`); 57 | }); 58 | } else { 59 | // 在开发环境下使用 HTTP 60 | server.listen(port, err => { 61 | if (err) throw err; 62 | // eslint-disable-next-line no-console 63 | console.log(`> Ready on http://localhost:${port}`); 64 | }); 65 | } 66 | } 67 | 68 | bootstrap(); 69 | -------------------------------------------------------------------------------- /src/app/[locale]/[...rest]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { notFound } from 'next/navigation'; 4 | 5 | export default function CatchAllPage() { 6 | notFound(); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/[locale]/agent/components/TagContent/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LeftOutlined, RightOutlined } from '@ant-design/icons'; 4 | import { Radio, RadioChangeEvent } from 'antd'; 5 | import classNames from 'classnames'; 6 | import { useTranslations } from 'next-intl'; 7 | import { useParams } from 'next/navigation'; 8 | import { useQueryState } from 'nuqs'; 9 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 10 | 11 | import { useStyles } from './styles'; 12 | 13 | interface TagContentProps { 14 | handleSelectTagChange: (tag: string) => void; 15 | selectedTag: string; 16 | cateList: any[]; 17 | } 18 | // 首先这里不能 memo, 用户存在使用过程中拖拽窗口大小 19 | const TagContent = ({ handleSelectTagChange, selectedTag, cateList = [] }: TagContentProps) => { 20 | const t = useTranslations(); 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | const [_, setQueryCategory] = useQueryState('category'); 23 | const { styles } = useStyles(); 24 | const scrollLeftRef = useRef(0); 25 | const [leftArrowVisible, setLeftArrowVisible] = useState(false); 26 | const [rightArrowVisible, setRightArrowVisible] = useState(false); 27 | const [windowWidth, setWindowWidth] = useState(0); 28 | const { locale } = useParams(); 29 | 30 | const handleScroll = useCallback((direction: string) => { 31 | const scrollAmount = window.innerWidth - 39; // You can adjust the scroll amount as needed 32 | const buttonList = document?.querySelector('#btns'); 33 | let left: number = 0; 34 | if (direction === 'left') { 35 | left = buttonList!.scrollLeft - scrollAmount; 36 | } else if (direction === 'right') { 37 | left = buttonList!.scrollLeft + scrollAmount; 38 | } 39 | buttonList?.scrollTo({ 40 | left, 41 | behavior: 'smooth', 42 | }); 43 | }, []); 44 | 45 | const onChange = useCallback( 46 | (e?: RadioChangeEvent) => { 47 | const value = e?.target?.value; 48 | if (e) { 49 | handleSelectTagChange(value); 50 | } 51 | // 获取事件源(按钮元素) 52 | const button: HTMLElement | null = document.querySelector( 53 | `[id='category_${value || selectedTag || ''}']` 54 | ); 55 | const scrollContainer: HTMLElement | null = document.querySelector('#btns'); 56 | 57 | if (button && scrollContainer) { 58 | // 获取按钮的位置信息 59 | const rect = button.getBoundingClientRect(); 60 | const containerRect = scrollContainer.getBoundingClientRect(); 61 | // 计算按钮中心点相对于滚动容器左侧的距离 62 | const buttonCenter = 63 | rect.left - 64 | containerRect.left + 65 | scrollContainer.scrollLeft + 66 | (button.offsetParent as HTMLElement)?.offsetWidth / 2; 67 | // 计算为了让按钮居中,容器应该滚动的目标位置 68 | const targetScrollLeft = buttonCenter - scrollContainer.offsetWidth / 2; 69 | // 确保目标滚动位置在有效范围内 70 | const safeTargetScrollLeft = Math.min( 71 | Math.max(targetScrollLeft, 0), 72 | scrollContainer.scrollWidth - scrollContainer.offsetWidth 73 | ); 74 | // 执行滚动操作 75 | scrollContainer.scrollTo({ 76 | left: safeTargetScrollLeft, 77 | behavior: 'smooth', 78 | }); 79 | } 80 | }, 81 | [handleSelectTagChange, selectedTag] 82 | ); 83 | useEffect(() => { 84 | // 获取包含内容的元素,例如 body 或任何带有横向滚动的容器 85 | const scrollContainer = document?.querySelector('#btns'); 86 | 87 | const resize = () => { 88 | setWindowWidth(window!.innerWidth || 0); 89 | // 小于879px的才需要出左右按钮,左侧菜单收起才出现左右按钮 90 | if (window?.innerWidth < 879) { 91 | const scrollLeft: number = scrollContainer!.scrollLeft || 0; 92 | setLeftArrowVisible(scrollLeft > 0); 93 | // 如果可视页面比容器小,说明需要出滚动条了,那么选中的tag也需要滚动到页面中间 94 | const visibleWidth = document?.documentElement.clientWidth; 95 | if (visibleWidth > scrollContainer!.scrollWidth) { 96 | setLeftArrowVisible(false); 97 | setRightArrowVisible(false); 98 | } else { 99 | setRightArrowVisible(true); 100 | onChange(); 101 | } 102 | } else { 103 | setLeftArrowVisible(false); 104 | setRightArrowVisible(false); 105 | } 106 | }; 107 | 108 | // 添加滚动事件监听器 109 | const scroll = () => { 110 | // 获取横向滚动的距离 111 | const scrollLeft: number = scrollContainer?.scrollLeft || 0; 112 | scrollLeftRef.current = scrollLeft; 113 | const visibleWidth = document?.documentElement.clientWidth; 114 | // 执行您的滚动事件处理逻辑 115 | setRightArrowVisible(scrollLeft + visibleWidth < scrollContainer!.scrollWidth); 116 | // 在滚动过程中,只要左边存在隐藏标签状态,就需要出现左移动按钮 117 | setLeftArrowVisible(scrollLeft > 0); 118 | }; 119 | scrollContainer?.addEventListener('scroll', scroll); 120 | window.addEventListener('resize', resize); 121 | // 由于初次刷新,按钮肯定没有选中过,此时如果需要判断左移动和右移动,可以手动调用resize函数 122 | resize(); // 如果初次渲染不想显示箭头,可以注释此行函数 123 | return () => { 124 | window.removeEventListener('resize', resize); 125 | scrollContainer?.removeEventListener('scroll', scroll); 126 | }; 127 | }, [scrollLeftRef]); 128 | return ( 129 | 130 | 131 | {leftArrowVisible && } 132 | {leftArrowVisible && ( 133 | handleScroll('left')} 136 | > 137 | 138 | 139 | )} 140 | {rightArrowVisible && } 141 | {rightArrowVisible && ( 142 | handleScroll('right')} 145 | > 146 | 147 | 148 | )} 149 | 150 | = 879 ? styles.btnListHidden : styles.btnListOverflow} 153 | id="btns" 154 | onChange={(e: RadioChangeEvent) => { 155 | const value = e.target.value; 156 | setQueryCategory(value || null); 157 | onChange(e); 158 | }} 159 | value={selectedTag} 160 | > 161 | {[ 162 | { id: '', name: t('components.index.tuiJian'), nameEn: t('components.index.tuiJian') }, 163 | ...cateList, 164 | ]?.map(item => ( 165 | 171 | {item[locale === 'zh' ? 'name' : 'nameEn']} 172 | 173 | ))} 174 | 175 | 176 | ); 177 | }; 178 | 179 | export default TagContent; 180 | -------------------------------------------------------------------------------- /src/app/[locale]/agent/components/TagContent/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | tagContent: { 5 | 'margin': `0 0 20px 0`, 6 | 'width': `100%`, 7 | '@media (max-width: 879px)': { 8 | '& .ant-radio-group': { 9 | display: 'flex !important', 10 | flexWrap: 'nowrap !important' as 'nowrap', 11 | }, 12 | }, 13 | '& .ant-radio-group': { 14 | display: 'inline-flex', 15 | gap: '8px', 16 | flexWrap: 'wrap', 17 | }, 18 | }, 19 | btnListOverflow: { 20 | display: 'flex', 21 | overflow: 'auto', 22 | }, 23 | btnListHidden: { 24 | display: 'box', 25 | overflow: 'hidden', 26 | }, 27 | 28 | arrows: {}, 29 | arrow: { 30 | 'color': 'rgb(204, 204, 204)', 31 | 'position': 'absolute', 32 | 'alignItems': 'center', 33 | 'cursor': 'pointer', 34 | 'display': 'flex', 35 | 'height': '30px', 36 | 'justifyContent': 'flex-end', 37 | 'zIndex': '3', 38 | 'width': '30px', 39 | '&:hover': { 40 | color: token.colorPrimary, 41 | cursor: 'pointer', 42 | }, 43 | }, 44 | shadowLeft: { 45 | position: 'absolute', 46 | background: `linear-gradient(to left,transparent,${token.colorBgBase} 100%)`, 47 | height: '36px', 48 | pointerEvents: 'none', 49 | width: '52px', 50 | left: '0', 51 | zIndex: '3', 52 | }, 53 | shadowRight: { 54 | position: 'absolute', 55 | background: `linear-gradient(to right,transparent,${token.colorBgBase} 100%)`, 56 | height: '36px', 57 | pointerEvents: 'none', 58 | width: '52px', 59 | right: '10px', 60 | zIndex: '3', 61 | }, 62 | left: { 63 | left: '0', 64 | }, 65 | right: { 66 | right: '10px', 67 | }, 68 | btn: { 69 | 'whiteSpace': 'nowrap', 70 | '&.ant-radio-button-wrapper': { 71 | borderInlineStart: `1px solid ${token.colorBorder}`, 72 | borderRadius: '12px', 73 | }, 74 | '&.ant-radio-button-wrapper:not(:first-child)::before': { 75 | backgroundColor: 'transparent', 76 | }, 77 | '&.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)': { 78 | borderColor: token.colorPrimary, 79 | }, 80 | }, 81 | })); 82 | -------------------------------------------------------------------------------- /src/app/[locale]/agent/components/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FireOutlined, PlusOutlined } from '@ant-design/icons'; 4 | import { sdk as bff } from '@yuntijs/arcadia-bff-sdk'; 5 | import { Button, Col, Row, Spin, Tooltip } from 'antd'; 6 | import classNames from 'classnames'; 7 | import { useTranslations } from 'next-intl'; 8 | import Image from 'next/image'; 9 | import Link from 'next/link'; 10 | import { useRouter } from 'next/navigation'; 11 | import { useQueryState } from 'nuqs'; 12 | import React, { useState } from 'react'; 13 | 14 | import TitleCom from '@/components/Title'; 15 | import { useAuthContext } from '@/layout/AuthLayout'; 16 | import { setLoginRedirect } from '@/utils/client'; 17 | 18 | import TagContent from './TagContent'; 19 | import { useStyles } from './styles'; 20 | 21 | interface AgentProps { 22 | agentData?: any; 23 | cateData?: any; 24 | } 25 | 26 | const layout = { 27 | xs: { 28 | span: 24, 29 | }, 30 | sm: { 31 | span: 12, 32 | }, 33 | md: { 34 | span: 12, 35 | }, 36 | lg: { 37 | span: 12, 38 | }, 39 | xl: { 40 | span: 8, 41 | }, 42 | xxl: { 43 | span: 6, 44 | }, 45 | }; 46 | 47 | const Agent = React.memo(({ agentData, cateData }) => { 48 | const t = useTranslations(); 49 | const { authed } = useAuthContext(); 50 | const router = useRouter(); 51 | const [category] = useQueryState('category'); 52 | const { styles } = useStyles(); 53 | // "",表示推荐标签" 54 | const [selectedTag, setSelectedTags] = useState(category || ''); 55 | // const [pageSize, setPageSize] = useState(-1); 56 | // const [page, setPage] = useState(1); 57 | const { data: ListData = [], loading } = bff.useListGpTs( 58 | { 59 | input: { 60 | category: selectedTag, 61 | page: 1, 62 | pageSize: -1, 63 | }, 64 | }, 65 | // agentData 是"推荐"选项的数据,所以非“推荐”选项的fallbackData数据就是[] 66 | { fallbackData: selectedTag ? [] : agentData } 67 | ); 68 | 69 | const handleSelectTagChange = tag => { 70 | setSelectedTags(tag); 71 | }; 72 | const getTitleExtra = () => { 73 | if (authed === undefined) return; // 未进行验证 74 | return authed ? ( 75 | } 77 | // onClick={() => router.push('/chat/bot/create')} 78 | size="large" 79 | type="primary" 80 | > 81 | {t('SideBarHeader.index.chuangJianZhiNengTi')} 82 | 83 | ) : ( 84 | router.push('/oidc/auth')}>{t('components.index.dengLu')} 85 | ); 86 | }; 87 | return ( 88 | 89 | 90 | 91 | 92 | 93 | 98 | 99 | 100 | 101 | {(ListData?.GPT?.listGPT?.nodes || []).map((item, index) => ( 102 | 103 | { 113 | if (!authed) { 114 | setLoginRedirect( 115 | `/chat/new?appNamespace=${item.name?.split( 116 | '/' 117 | )?.[0]}&appName=${item.name?.split('/')?.[1]}` 118 | ); 119 | } 120 | }} 121 | > 122 | 123 | 124 | 125 | 126 | {item.displayName} 127 | 128 | {item.description || '-'} 129 | 130 | 131 | 132 | {item.hot} w 133 | 134 | @{item.creator} 135 | 136 | 137 | 138 | 139 | ))} 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ); 148 | }); 149 | 150 | export default Agent; 151 | -------------------------------------------------------------------------------- /src/app/[locale]/agent/components/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | agentContainer: { 5 | 'width': '100%', 6 | 'position': 'relative', 7 | 'backgroundColor': token.colorBgBase, 8 | '& > div': { 9 | height: '100%', 10 | }, 11 | }, 12 | agentContent: { 13 | margin: '0 auto', 14 | }, 15 | main: { 16 | height: 'calc(100vh - 84px)', 17 | padding: '64px 24px 24px 24px', 18 | }, 19 | content: {}, 20 | tag: { 21 | minWidth: '108px', 22 | textAlign: 'center', 23 | height: '36px', 24 | lineHeight: '36px', 25 | border: '1px solid #E5E5E5', 26 | fontSize: '14px', 27 | marginBottom: 8, 28 | }, 29 | card: { 30 | display: 'flex', 31 | backgroundColor: token.colorBgLayout, 32 | padding: 16, 33 | borderRadius: '16px', 34 | cursor: 'pointer', 35 | }, 36 | left: { 37 | height: '72px', 38 | width: '72px', 39 | marginRight: 12, 40 | img: { 41 | objectFit: 'cover', 42 | verticalAlign: 'middle', 43 | borderRadius: 12, 44 | }, 45 | }, 46 | right: { 47 | flex: 1, 48 | width: `calc(100% - 75px)`, 49 | }, 50 | title: { 51 | marginTop: '5px', 52 | fontSize: '15px', 53 | whiteSpace: 'nowrap', 54 | overflow: 'hidden', 55 | textOverflow: 'ellipsis', 56 | fontWeight: 700, 57 | color: token.colorTextBase, 58 | }, 59 | desc: { 60 | color: token.colorTextDescription, 61 | fontSize: 14, 62 | whiteSpace: 'nowrap', 63 | overflow: 'hidden', 64 | textOverflow: 'ellipsis', 65 | }, 66 | info: { 67 | fontSize: 12, 68 | marginTop: 4, 69 | color: token.colorTextDescription, 70 | display: 'flex', 71 | }, 72 | heat: { 73 | width: 50, 74 | }, 75 | creator: { 76 | flex: 1, 77 | }, 78 | })); 79 | -------------------------------------------------------------------------------- /src/app/[locale]/agent/page.tsx: -------------------------------------------------------------------------------- 1 | import { sdk as bff } from '@yuntijs/arcadia-bff-sdk'; 2 | import { Flex } from 'antd'; 3 | import { getTranslations } from 'next-intl/server'; 4 | import React from 'react'; 5 | 6 | import Agent from './components'; 7 | 8 | export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { 9 | const t = await getTranslations({ locale }); 10 | return { 11 | title: t('components.index.faXianAIZhi'), 12 | }; 13 | } 14 | 15 | export default async function Page() { 16 | const agentData = await bff 17 | .listGPTs({ 18 | input: { 19 | page: 1, 20 | pageSize: 20, 21 | }, 22 | }) 23 | .catch(error => { 24 | console.warn('getAgent failed', error); 25 | }); 26 | 27 | const cateData = await bff.listGPTCategory().catch(error => { 28 | console.warn('getGPTCategory failed', error); 29 | }); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/[locale]/chat/(conversation)/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Chat from '@yuntijs/chat'; 4 | import { createStyles } from 'antd-style'; 5 | import React from 'react'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | import { useSelector } from 'react-redux'; 8 | 9 | export const useStyles = createStyles(({ token }) => ({ 10 | conversationWrapper: { 11 | position: 'relative', 12 | backgroundColor: token.colorBgLayout, 13 | }, 14 | chatContainer: { 15 | width: '100%', 16 | }, 17 | })); 18 | 19 | interface ConversationProps { 20 | params?: any; 21 | searchParams?: any; 22 | } 23 | 24 | const Conversation = React.memo(({ params, searchParams }) => { 25 | const { styles } = useStyles(); 26 | const theme = useSelector((store: any) => store.theme); 27 | const isDark = 28 | theme === 'dark' || 29 | (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); 30 | 31 | return ( 32 | <> 33 | 34 | 35 | 42 | 43 | 44 | > 45 | ); 46 | }); 47 | 48 | export default Conversation; 49 | -------------------------------------------------------------------------------- /src/app/[locale]/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createStyles } from 'antd-style'; 4 | import dynamic from 'next/dynamic'; 5 | import React from 'react'; 6 | 7 | // import Loading from '@/components/Loading'; 8 | // import Conversation from '../(conversation)'; 9 | 10 | const useStyles = createStyles(({ token, css }) => { 11 | return { 12 | containers: css` 13 | position: relative; 14 | width: 100%; 15 | background-color: ${token.colorBgBase}; 16 | `, 17 | }; 18 | }); 19 | 20 | const Conversation = dynamic(() => import('../(conversation)'), { 21 | ssr: false, // 禁用服务端渲染 22 | // loading: () => , 23 | }); 24 | 25 | export default function Chat(props: { params: any; searchParams: any }) { 26 | const { styles } = useStyles(); 27 | return ( 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/[locale]/chat/bot/create/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => { 4 | return { 5 | BotCreate: { 6 | width: '100%', 7 | background: token.colorBgLayout, 8 | position: 'relative', 9 | }, 10 | content: { 11 | 'paddingTop': '64px', 12 | 'paddingBottom': '24px', 13 | '.ant-input, .ant-select-selector': { 14 | borderColor: 'transparent !important', 15 | }, 16 | '.ant-input:focus, .ant-select-focused >.ant-select-selector': { 17 | borderColor: `${token.colorPrimaryActive} !important`, 18 | }, 19 | }, 20 | uploadText: { 21 | 'border': 0, 22 | 'background': 'none', 23 | '& > div': { 24 | marginTop: 4, 25 | }, 26 | }, 27 | avatarImg: { 28 | borderRadius: '50%', 29 | }, 30 | leftContent: { 31 | width: '40%', 32 | padding: '24px 40px 0 60px', 33 | }, 34 | rightContent: { 35 | width: '60%', 36 | background: token.colorWhite, 37 | }, 38 | tag: { 39 | position: 'relative', 40 | top: '20px', 41 | left: '40px', 42 | }, 43 | uploadAvatar: { 44 | textAlign: 'center', 45 | }, 46 | uploadFile: { 47 | background: token.colorWhite, 48 | color: token.colorTextPlaceholder, 49 | padding: '10px 20px 10px 20px', 50 | borderRadius: token.borderRadius, 51 | }, 52 | uploadIcon: { 53 | marginRight: '4px', 54 | }, 55 | menuIcon: { 56 | marginRight: '4px', 57 | color: token.colorPrimary, 58 | }, 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/[locale]/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | import { ReactNode } from 'react'; 5 | 6 | import { useAuthContext } from '@/layout/AuthLayout'; 7 | 8 | type Props = { 9 | children: ReactNode; 10 | }; 11 | 12 | // Since we have a `not-found.tsx` page on the root, a layout file 13 | // is required, even if it's just passing children through. 14 | export default function RootLayout({ children }: Props) { 15 | const { authed } = useAuthContext(); 16 | if (authed === false) { 17 | // 无效认证 18 | redirect('/agent'); 19 | } 20 | return children; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/[locale]/chat/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // import dynamic from 'next/dynamic'; 4 | import { useParams, useRouter } from 'next/navigation'; 5 | import React from 'react'; 6 | 7 | import EmptyPage from '@/components/EmptyPage'; 8 | import { useAxiosRequest } from '@/utils/axios'; 9 | import { DEFAULT_CHAT } from '@/utils/constants'; 10 | 11 | // const Conversation = dynamic( 12 | // () => import('./(conversation)'), 13 | // { ssr: false } // 禁用服务端渲染 14 | // ); 15 | 16 | export default function Chat() { 17 | const router = useRouter(); 18 | const { locale } = useParams(); 19 | const [{ data }] = useAxiosRequest({ 20 | url: '/kubeagi-apis/gpts/chat/conversations', 21 | method: 'POST', 22 | }); 23 | React.useEffect(() => { 24 | const first_chat = data?.[0]; 25 | if (first_chat?.id && first_chat?.id !== DEFAULT_CHAT) { 26 | router.push( 27 | `/${locale}/chat/${first_chat?.id}?appName=${first_chat?.app_name}&appNamespace=${first_chat?.app_namespace}` 28 | ); 29 | } 30 | }, [data]); 31 | return ( 32 | <> 33 | {/* 暂时屏蔽, 暂无默认 chat */} 34 | {/* */} 35 | 36 | > 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Viewport } from 'next'; 2 | import { NextIntlClientProvider, useMessages } from 'next-intl'; 3 | import { cookies } from 'next/headers'; 4 | import Script from 'next/script'; 5 | import React from 'react'; 6 | 7 | import AppLayoutTemplate from '@/layout/AppLayoutTemplate'; 8 | import AuthLayout from '@/layout/AuthLayout'; 9 | import AxiosConfigLayout from '@/layout/AxiosConfigLayout'; 10 | import GlobalLayout from '@/layout/GlobalLayout'; 11 | import PWAHandlerLayout from '@/layout/PWAHandlerLayout'; 12 | import StyleRegistry from '@/layout/StyleRegistry'; 13 | 14 | export { generateMetadata } from './metadata'; 15 | 16 | export const viewport: Viewport = { 17 | themeColor: '#FFFFFF', 18 | initialScale: 1, 19 | maximumScale: 1, 20 | userScalable: false, 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | params: { locale }, 26 | }: { 27 | children: React.ReactNode; 28 | params: { locale: string }; 29 | }) { 30 | const messages = useMessages(); 31 | // dir === ltr | rtl 32 | return ( 33 | 34 | 35 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 69 | {children} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/app/[locale]/logout/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function Logout() { 4 | redirect('/oidc/logout'); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/[locale]/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | import { getOriginServerSide } from '@/utils'; 4 | 5 | const APP_NAME = 'AgileGPT'; 6 | const APP_DEFAULT_TITLE = APP_NAME; 7 | const APP_TITLE_TEMPLATE = '%s - ' + APP_NAME; 8 | const APP_DESCRIPTION = 'Agile GPTs'; 9 | 10 | const metadata: Metadata = { 11 | applicationName: APP_NAME, 12 | metadataBase: new URL('http://localhost:3000'), 13 | title: { 14 | default: APP_DEFAULT_TITLE, 15 | template: APP_TITLE_TEMPLATE, 16 | }, 17 | description: APP_DESCRIPTION, 18 | manifest: '/manifest.json', 19 | appleWebApp: { 20 | capable: true, 21 | statusBarStyle: 'default', 22 | title: APP_DEFAULT_TITLE, 23 | // startUpImage: [], 24 | }, 25 | formatDetection: { 26 | telephone: false, 27 | }, 28 | openGraph: { 29 | type: 'website', 30 | // metadataBase: 'https://tenxcloud.com', 31 | siteName: APP_NAME, 32 | title: { 33 | default: APP_DEFAULT_TITLE, 34 | template: APP_TITLE_TEMPLATE, 35 | }, 36 | description: APP_DESCRIPTION, 37 | }, 38 | twitter: { 39 | card: 'summary', 40 | title: { 41 | default: APP_DEFAULT_TITLE, 42 | template: APP_TITLE_TEMPLATE, 43 | }, 44 | description: APP_DESCRIPTION, 45 | }, 46 | }; 47 | 48 | // type Props = { 49 | // params: { id: string }; 50 | // searchParams: { [key: string]: string | string[] | undefined }; 51 | // }; 52 | 53 | export async function generateMetadata(): Promise { 54 | const origin = getOriginServerSide(); 55 | return Object.assign( 56 | metadata, 57 | origin 58 | ? { 59 | metadataBase: new URL(origin), 60 | } 61 | : {} 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/app/[locale]/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from 'antd'; 4 | import { useTranslations } from 'next-intl'; 5 | import Image from 'next/image'; 6 | import { useRouter } from 'next/navigation'; 7 | import * as React from 'react'; 8 | 9 | import { useStyles } from '../../styles/not-found-styles'; 10 | 11 | const NotFound = () => { 12 | const { styles } = useStyles(); 13 | const router = useRouter(); 14 | const t = useTranslations('app'); 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | {t('not_found.henBaoQianYeMian')} 22 | 23 | { 26 | router.push(`/`); 27 | }} 28 | size="large" 29 | type="primary" 30 | > 31 | {t('not_found.fanHuiShouYe')} 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default NotFound; 40 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/auth/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | import queryString from 'query-string'; 5 | 6 | import oidc from '@/config/oidc.mjs'; 7 | import { getOriginServerSide } from '@/utils'; 8 | 9 | const { client, server } = oidc; 10 | const { url } = server; 11 | const { client_id, redirect_uri } = client; 12 | 13 | export default async function AuthServer() { 14 | const origin = getOriginServerSide(); 15 | const query = queryString.stringify({ 16 | client_id, 17 | redirect_uri: `${origin}${redirect_uri}`, 18 | response_type: 'code', 19 | scope: 'openid profile email groups offline_access', 20 | }); 21 | redirect(`${url}/auth?${query}`); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/callback/Callback.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { notification } from 'antd'; 4 | import { useTranslations } from 'next-intl'; 5 | import { useRouter } from 'next/navigation'; 6 | import React from 'react'; 7 | import { useDispatch } from 'react-redux'; 8 | 9 | import { delLoginRedirect, getLoginRedirect } from '@/utils/client'; 10 | import { AUTH_DATA } from '@/utils/constants'; 11 | 12 | export default function Callback({ data: res }: { data: any }) { 13 | const dispatch = useDispatch(); 14 | const t = useTranslations('callback'); 15 | const router = useRouter(); 16 | const saveAuth = async () => { 17 | const redirectUrl: string = getLoginRedirect(document.cookie); 18 | if (redirectUrl) { 19 | delLoginRedirect(); 20 | } 21 | if (res?.data?.errors || !res?.data) { 22 | console.warn(res?.data?.errors); 23 | notification.warning({ 24 | message: t('Callback.renZhengShiBaiQing'), 25 | }); 26 | setTimeout(() => { 27 | router.push('/oidc/logout'); 28 | }, 5000); 29 | return; 30 | } 31 | if (res?.data) { 32 | localStorage.setItem( 33 | AUTH_DATA, 34 | JSON.stringify({ 35 | token: res.data, 36 | }) 37 | ); 38 | dispatch({ 39 | type: 'SAVE_AUTH_DATA', 40 | authData: res.data, 41 | }); 42 | router.push(redirectUrl || '/'); 43 | } 44 | }; 45 | React.useEffect(() => { 46 | saveAuth(); 47 | }, []); 48 | return <> >; 49 | } 50 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/callback/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import axios from 'axios'; 4 | import https from 'https'; 5 | import React from 'react'; 6 | 7 | import oidc from '@/config/oidc.mjs'; 8 | import { getOriginServerSide } from '@/utils'; 9 | 10 | import Callback from './Callback'; 11 | 12 | export default async function CallbackServer(props: any) { 13 | const { searchParams } = props; 14 | const { code } = searchParams; 15 | const { redirect_uri } = oidc.client; 16 | const origin = getOriginServerSide(); 17 | const httpsAgent = new https.Agent({ 18 | rejectUnauthorized: false, 19 | }); 20 | const res: any = await axios.post( 21 | `${origin}/oidc/token?code=${code}&redirect_uri=${origin}${redirect_uri}`, 22 | {}, 23 | { 24 | httpsAgent, 25 | } 26 | ); 27 | return ; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Flex, Spin } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import { useTranslations } from 'next-intl'; 6 | import { useParams, usePathname } from 'next/navigation'; 7 | import React from 'react'; 8 | 9 | import LogoSvg from '@/../public/svg/logo.svg'; 10 | 11 | export const useStyles = createStyles(({ token }) => ({ 12 | wrapper: { 13 | height: '100vh', 14 | width: '100vw', 15 | position: 'absolute', 16 | left: '0', 17 | zIndex: '10', 18 | backgroundColor: token.colorBgBase, 19 | }, 20 | spin: { 21 | minWidth: '60px', 22 | svg: { 23 | fill: token.colorTextBase, 24 | }, 25 | }, 26 | logo: { 27 | width: '140px !important', 28 | height: '32px !important', 29 | left: '50%', 30 | margin: 0, 31 | transform: 'translateX(-50%)', 32 | marginTop: '-30px !important', 33 | }, 34 | })); 35 | 36 | export default function Layout({ children }: { children: React.ReactNode }) { 37 | const pathname = usePathname(); 38 | const { styles } = useStyles(); 39 | const t = useTranslations('oidc'); 40 | const { locale } = useParams(); 41 | const tip = React.useMemo(() => { 42 | switch (pathname) { 43 | case `/${locale}/oidc/logout`: { 44 | return; 45 | // return t('layout.dengChuZhong'); 46 | } 47 | default: { 48 | return t('layout.jiaZaiZhong'); 49 | } 50 | } 51 | }, [pathname]); 52 | return ( 53 | 54 | } 57 | spinning={!!tip} 58 | tip={{tip}} 59 | > 60 | {children} 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/logout/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import React from 'react'; 5 | 6 | import { setAxiosHooksWithoutAuth } from '@/utils/axios'; 7 | import { AUTH_DATA } from '@/utils/constants'; 8 | 9 | export default function Logout() { 10 | const router = useRouter(); 11 | React.useEffect(() => { 12 | setAxiosHooksWithoutAuth(); 13 | window.localStorage.removeItem(AUTH_DATA); 14 | // router.push('/oidc/remove-auth-and-login'); // 暂时屏蔽登出, 参考, 跳转到未登录的状态 15 | router.push('/'); 16 | }); 17 | return <>>; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/page.tsx: -------------------------------------------------------------------------------- 1 | export { default } from '../page'; 2 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/remove-auth-and-login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { redirect } from 'next/navigation'; 4 | 5 | import oidc from '@/config/oidc.mjs'; 6 | import { getOriginServerSide } from '@/utils'; 7 | 8 | export default async function LogoutServer() { 9 | const origin = getOriginServerSide(); 10 | const { client, server } = oidc; 11 | const { redirect_uri } = client; 12 | const { url } = server; 13 | 14 | redirect( 15 | `${url}/logout/remove-auth-data?redirect=${encodeURIComponent( 16 | `${url}/auth?redirect_uri=${origin}${redirect_uri}&response_type=code&scope=openid+profile+email+groups+offline_access` 17 | )}` 18 | ); 19 | return <>>; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/[locale]/oidc/token/route.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import axios from 'axios'; 4 | import https from 'https'; 5 | import { type NextRequest, NextResponse } from 'next/server'; 6 | 7 | import oidc from '@/config/oidc.mjs'; 8 | import { btoa } from '@/utils'; 9 | 10 | const { client, server } = oidc; 11 | const { url } = server; 12 | const { client_id, client_secret } = client; 13 | 14 | export async function POST(request: NextRequest) { 15 | const searchParams = request.nextUrl.searchParams; 16 | const code = searchParams.get('code'); 17 | const redirect_uri = searchParams.get('redirect_uri'); 18 | const body = { 19 | grant_type: 'authorization_code', 20 | code, 21 | redirect_uri, 22 | }; 23 | const httpsAgent = new https.Agent({ 24 | rejectUnauthorized: false, 25 | }); 26 | const res: any = await axios.post(`${url}/token`, body, { 27 | headers: { 28 | 'Authorization': `Basic ${btoa(`${client_id}:${client_secret}`)}`, 29 | 'Content-Type': 'application/json', 30 | }, 31 | httpsAgent, 32 | timeout: 10_000, 33 | }); 34 | return NextResponse.json({ data: res.data }); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import React from 'react'; 5 | 6 | import EmptyPage from '@/components/EmptyPage'; 7 | import { useAuthContext } from '@/layout/AuthLayout'; 8 | 9 | export default function RootPage(props: { params: { locale: string } }) { 10 | const locale = props?.params?.locale; 11 | const router = useRouter(); 12 | const { authed } = useAuthContext(); 13 | React.useEffect(() => { 14 | if (authed === undefined) return; // 未验证 15 | if (authed) { 16 | // 有效认证 17 | router.push(`${locale ? `/${locale}` : ''}/chat`); 18 | } else { 19 | // 无效认证 20 | router.push(`${locale ? `/${locale}` : ''}/agent`); 21 | } 22 | }, [authed]); 23 | return ; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/BtnList/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Bot, 3 | Cog, 4 | Info, 5 | LogOut, 6 | MessageCircleMore, 7 | MonitorDown, 8 | Settings, 9 | Share2, 10 | User2Icon, 11 | } from 'lucide-react'; 12 | import { useTranslations } from 'next-intl'; 13 | import { useRouter } from 'next/navigation'; 14 | import React from 'react'; 15 | 16 | import BtnsBlock, { Btn } from '@/components/BtnsBlock'; 17 | import { useInstallPrompt } from '@/layout/PWAHandlerLayout'; 18 | 19 | import { useStyles } from './styles'; 20 | 21 | // interface SettingBtnListProps {} 22 | 23 | const SettingBtnList = React.memo(() => { 24 | const router = useRouter(); 25 | const t = useTranslations(); 26 | const { styles, theme } = useStyles(); 27 | const installPrompt: any = useInstallPrompt(); 28 | const btnsUser: Btn[] = React.useMemo( 29 | () => [ 30 | { 31 | icon: User2Icon, 32 | title: t('BtnList.index.geRenZiLiao'), 33 | href: '/setting/user-info', 34 | }, 35 | { 36 | icon: Cog, 37 | title: t('DataControlClient.index.shuJuKongZhi'), 38 | href: '/setting/data-control', 39 | }, 40 | { 41 | icon: Settings, 42 | title: t('AccountClient.index.zhangHaoSheZhi'), 43 | href: '/setting/account', 44 | }, 45 | ], 46 | [] 47 | ); 48 | 49 | const btnsMy: Btn[] = React.useMemo( 50 | () => [ 51 | { 52 | icon: Bot, 53 | icon_bg: theme.colorSuccess, 54 | title: t('BtnList.index.woDeZhiNengTi'), 55 | href: '/', 56 | }, 57 | ], 58 | [] 59 | ); 60 | 61 | const btnsActions: Btn[] = React.useMemo( 62 | () => [ 63 | { 64 | icon: Share2, 65 | title: t('BtnList.index.fenXiang'), 66 | }, 67 | { 68 | icon: MonitorDown, 69 | title: t('BtnList.index.tianJiaZhiZhuoMian'), 70 | onClick: () => { 71 | if (installPrompt) { 72 | console.warn('installPrompt', installPrompt); 73 | installPrompt.prompt(); 74 | 75 | // 等待用户做出选择 76 | installPrompt.userChoice.then( 77 | (choiceResult: { outcome: 'accepted' | 'dismissed'; platform: string }) => { 78 | if (choiceResult.outcome === 'accepted') { 79 | // console.warn('用户接受了安装应用'); 80 | } else { 81 | // console.warn('用户拒绝了安装应用'); 82 | } 83 | 84 | // 清除 installPrompt,因为它不能被重用 85 | // setInstallPrompt(null); 86 | } 87 | ); 88 | } 89 | }, 90 | }, 91 | { 92 | icon: MessageCircleMore, 93 | title: t('BtnList.index.shiYongFanKui'), 94 | }, 95 | { 96 | icon: Info, 97 | title: t('BtnList.index.guanYu'), 98 | href: '/test', 99 | }, 100 | ], 101 | [installPrompt] 102 | ); 103 | 104 | const btnsSetting: Btn[] = React.useMemo( 105 | () => [ 106 | { 107 | icon: LogOut, 108 | title: t('BtnList.index.tuiChuDengLu'), 109 | onClick: () => { 110 | router.push('/oidc/logout'); 111 | }, 112 | }, 113 | ], 114 | [] 115 | ); 116 | return ( 117 | 118 | 119 | 120 | 121 | 122 | 123 | ); 124 | }); 125 | 126 | export default SettingBtnList; 127 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/BtnList/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(() => ({ 4 | btnlist: { 5 | marginBottom: 24, 6 | }, 7 | })); 8 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/UserInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { UserOutlined } from '@ant-design/icons'; 2 | import type { GetCurrentUserQuery } from '@yuntijs/bff-client'; 3 | import { Avatar, Skeleton } from 'antd'; 4 | import classNames from 'classnames'; 5 | import React from 'react'; 6 | 7 | import { useStyles } from './styles'; 8 | 9 | interface SettingUserInfoProps { 10 | user: GetCurrentUserQuery['userCurrent']; 11 | } 12 | 13 | const SettingUserInfo = React.memo(({ user }) => { 14 | const { styles } = useStyles(); 15 | return user?.name ? ( 16 | 17 | } size={100} /> 18 | {user?.name} 19 | id: {user?.name} 20 | 21 | ) : ( 22 | 23 | 29 | 30 | 34 | 35 | ); 36 | }); 37 | 38 | export default SettingUserInfo; 39 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/UserInfo/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(() => ({ 4 | userinfo: { 5 | textAlign: 'center', 6 | marginBottom: 24, 7 | height: 164, 8 | overflow: 'hidden', 9 | }, 10 | name: { 11 | fontSize: '18px', 12 | fontWeight: 500, 13 | marginTop: '16px', 14 | }, 15 | userid: { 16 | color: '#999', 17 | fontSize: '12px', 18 | fontWeight: 500, 19 | }, 20 | emptyline: { 21 | width: '120px', 22 | marginLeft: 'auto', 23 | marginRight: 'auto', 24 | }, 25 | emptylineUserid: { 26 | 'height': 18, 27 | '.ant-skeleton-title': { 28 | marginBottom: 'unset', 29 | }, 30 | }, 31 | avatorSkeleton: { 32 | '.ant-skeleton-header': { 33 | paddingInlineEnd: 'unset', 34 | }, 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type GetCurrentUserQuery, sdk } from '@yuntijs/bff-client'; 4 | import React from 'react'; 5 | 6 | import ReturnBtn from '@/components/ReturnBtn'; 7 | 8 | import BtnList from './BtnList'; 9 | import UserInfo from './UserInfo'; 10 | import { useStyles } from './styles'; 11 | 12 | interface SettingProps { 13 | userData?: GetCurrentUserQuery; 14 | } 15 | 16 | const Setting = React.memo(() => { 17 | const { styles } = useStyles(); 18 | const { data } = sdk.useGetCurrentUser(); 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }); 33 | 34 | export default Setting; 35 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/SettingClient/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | setting: { 5 | 'width': '100%', 6 | 'backgroundColor': token.colorBgLayout, 7 | 'position': 'relative', 8 | '& > div': { 9 | height: '100%', 10 | }, 11 | }, 12 | content: { 13 | maxWidth: '600px', 14 | margin: '0 auto', 15 | padding: '64px 16px 24px 16px', 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/account/AccountClient/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { sdk } from '@yuntijs/bff-client'; 4 | import { App, Flex, Skeleton, SkeletonProps, Switch } from 'antd'; 5 | import classNames from 'classnames'; 6 | import { useTranslations } from 'next-intl'; 7 | import React from 'react'; 8 | 9 | import BtnsBlock, { Btn } from '@/components/BtnsBlock'; 10 | import ReturnBtn from '@/components/ReturnBtn'; 11 | 12 | import { useStyles } from './styles'; 13 | 14 | interface Props { 15 | url: string | undefined; 16 | } 17 | 18 | const AccountClient: React.FC = ({ url }) => { 19 | const { styles } = useStyles(); 20 | const t = useTranslations('AccountClient'); 21 | const [checked, setChecked] = React.useState(false); 22 | const { modal } = App.useApp(); 23 | const { data } = sdk.useGetCurrentUser(); 24 | const user = data?.userCurrent; 25 | const skeletonProps: SkeletonProps = { 26 | paragraph: false, 27 | style: { 28 | width: 120, 29 | }, 30 | }; 31 | const btns1: Btn[] = React.useMemo( 32 | () => [ 33 | { 34 | title: t('index.geXingHuaNeiRong'), 35 | action: ( 36 | { 39 | setChecked(_checked); 40 | }} 41 | /> 42 | ), 43 | }, 44 | ], 45 | [checked] 46 | ); 47 | const btnsUser: Btn[] = React.useMemo( 48 | () => [ 49 | { 50 | title: t('index.shouJi'), 51 | btn_extra: user?.phone || , 52 | onClick: () => { 53 | window.open(`${url}/management/account`); 54 | }, 55 | }, 56 | { 57 | title: t('index.weiXin'), 58 | // btn_extra: user?.phone, 59 | onClick: () => { 60 | // dynamic BtnsBlock 会闪一下(没有 block 占位), 再细分 dynamic ? 61 | modal.confirm({ 62 | title: user?.name, 63 | }); 64 | }, 65 | }, 66 | { 67 | title: t('index.youXiang'), 68 | btn_extra: user?.email || , 69 | onClick: () => { 70 | window.open(`${url}/management/account`); 71 | }, 72 | }, 73 | ], 74 | [user, modal] 75 | ); 76 | const btns_del_all: Btn[] = React.useMemo( 77 | () => [ 78 | { 79 | title: t('index.zhuXiaoZhangHu'), 80 | danger: true, 81 | onClick: () => { 82 | console.warn('delete user'); 83 | }, 84 | }, 85 | ], 86 | [checked] 87 | ); 88 | return ( 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | 104 | export default AccountClient; 105 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/account/AccountClient/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | account: { 5 | 'height': '100%', 6 | 'width': '100%', 7 | 'backgroundColor': token.colorBgLayout, 8 | 'position': 'relative', 9 | '& > div': { 10 | position: 'relative', 11 | overflow: 'hidden', 12 | height: '100%', 13 | paddingBottom: '40px', 14 | paddingTop: '64px', 15 | }, 16 | '.ant-skeleton-title': { 17 | marginBottom: 'unset', 18 | }, 19 | }, 20 | sub: { 21 | width: '100%', 22 | }, 23 | content: { 24 | paddingTop: 16, 25 | paddingBottom: 42, 26 | width: 600, 27 | }, 28 | table: { 29 | '.ant-table-tbody > tr:last-child > td': { 30 | borderBottom: 'unset', 31 | }, 32 | }, 33 | })); 34 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/account/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import React from 'react'; 3 | 4 | import oidc from '@/config/oidc.mjs'; 5 | 6 | import AccountClient from './AccountClient'; 7 | 8 | const { server } = oidc; 9 | const { url } = server; 10 | 11 | export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { 12 | const t = await getTranslations({ locale, namespace: 'AccountClient' }); 13 | return { 14 | title: t('index.zhangHaoSheZhi'), 15 | }; 16 | } 17 | 18 | export default async function SettingAccountPage() { 19 | const props = { 20 | url, 21 | }; 22 | return ( 23 | <> 24 | 25 | > 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/data-control/DataControlClient/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button, Card, Flex, Switch, Table } from 'antd'; 4 | import classNames from 'classnames'; 5 | import { useTranslations } from 'next-intl'; 6 | import React from 'react'; 7 | 8 | import BtnsBlock, { Btn } from '@/components/BtnsBlock'; 9 | import ReturnBtn from '@/components/ReturnBtn'; 10 | 11 | import { useStyles } from './styles'; 12 | 13 | interface Props { 14 | user?: { 15 | name: string; 16 | }; 17 | } 18 | 19 | const DataControlClient: React.FC = () => { 20 | const { styles } = useStyles(); 21 | const [checked, setChecked] = React.useState(false); 22 | const t = useTranslations('DataControlClient'); 23 | 24 | const btns1: Btn[] = React.useMemo( 25 | () => [ 26 | { 27 | title: t('index.liaoTianJiLuYing'), 28 | action: ( 29 | { 32 | setChecked(_checked); 33 | }} 34 | /> 35 | ), 36 | }, 37 | ], 38 | [checked] 39 | ); 40 | const btns_del_all: Btn[] = React.useMemo( 41 | () => [ 42 | { 43 | title: t('index.shanChuSuoYouLiao'), 44 | danger: true, 45 | onClick: () => { 46 | console.warn('handel del all'); 47 | }, 48 | }, 49 | ], 50 | [checked] 51 | ); 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | { 79 | return ( 80 | 81 | {t('index.shanChu')} 82 | 83 | ); 84 | }, 85 | }, 86 | ]} 87 | dataSource={[ 88 | { 89 | name: t('index.duiHua'), 90 | time: '2024-01-01 08:08:08', 91 | }, 92 | ]} 93 | pagination={false} 94 | /> 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default DataControlClient; 104 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/data-control/DataControlClient/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | dataControl: { 5 | 'height': '100%', 6 | 'width': '100%', 7 | 'backgroundColor': token.colorBgLayout, 8 | 'position': 'relative', 9 | '& > div': { 10 | position: 'relative', 11 | overflow: 'hidden', 12 | height: '100%', 13 | paddingBottom: '40px', 14 | paddingTop: '64px', 15 | }, 16 | }, 17 | sub: { 18 | width: '100%', 19 | }, 20 | content: { 21 | paddingTop: 16, 22 | paddingBottom: 42, 23 | width: 600, 24 | }, 25 | table: { 26 | '.ant-table-tbody > tr:last-child > td': { 27 | borderBottom: 'unset', 28 | }, 29 | }, 30 | })); 31 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/data-control/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, memo } from 'react'; 2 | 3 | const Layout = memo(({ children }) => { 4 | return <>{children}>; 5 | }); 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/data-control/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import React from 'react'; 3 | 4 | import DataControlClient from './DataControlClient'; 5 | 6 | export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { 7 | const t = await getTranslations({ locale, namespace: 'DataControlClient' }); 8 | return { 9 | title: t('index.shuJuKongZhi'), 10 | }; 11 | } 12 | 13 | export default async function SettingDataControlPage() { 14 | // const user = await getUserData(); 15 | const props = { 16 | // user, 17 | }; 18 | return ( 19 | <> 20 | 21 | > 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import React from 'react'; 3 | 4 | import SettingClient from './SettingClient'; 5 | 6 | export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { 7 | const t = await getTranslations({ locale, namespace: 'setting' }); 8 | return { 9 | title: t('page.geRenSheZhi'), 10 | }; 11 | } 12 | 13 | export default async function SettingPage() { 14 | // swr SSR example, will be removed in the future 15 | // see https://github.com/vercel/swr/blob/main/examples/server-render/pages/index.js 16 | // const userData = await sdk 17 | // .getCurrentUser(undefined, { 18 | // Authorization: 'bearer ', 19 | // }) 20 | // .catch(error => { 21 | // console.warn('getCurrentUser failed', error); 22 | // }); 23 | 24 | return ( 25 | <> 26 | 27 | > 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/user-info/UserInfoClient/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'; 4 | import { Button, Flex, Form, Input, Upload } from 'antd'; 5 | import type { UploadFile } from 'antd'; 6 | import classNames from 'classnames'; 7 | import { useTranslations } from 'next-intl'; 8 | import Image from 'next/image'; 9 | import React, { useState } from 'react'; 10 | 11 | import ReturnBtn from '@/components/ReturnBtn'; 12 | 13 | import { useStyles } from './styles'; 14 | 15 | const getBase64 = (img: any, callback: (url: string) => void) => { 16 | const reader = new FileReader(); 17 | reader.addEventListener('load', () => callback(reader.result as string)); 18 | reader.readAsDataURL(img); 19 | }; 20 | 21 | interface Props { 22 | user?: { 23 | name: string; 24 | }; 25 | } 26 | 27 | const normFile = (e: any) => { 28 | if (Array.isArray(e)) { 29 | return e; 30 | } 31 | if (!e?.fileList?.length) { 32 | return []; 33 | } 34 | return [e?.fileList[e?.fileList?.length - 1]]; 35 | }; 36 | 37 | const onFinish = () => {}; 38 | const UserInfoClient: React.FC = () => { 39 | const { styles } = useStyles(); 40 | const t = useTranslations(); 41 | const [loading, setLoading] = useState(false); 42 | const [imageUrl, setImageUrl] = useState(); 43 | const [form] = Form.useForm(); 44 | 45 | const uploadButton = ( 46 | 47 | {loading ? : } 48 | {t('create.page.shangChuanTouXiang')} 49 | 50 | ); 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | { 62 | // const isLt10M = file.size / 1024 / 1024 <= 5; 63 | // if (!isLt10M) { 64 | // notification.warn({ 65 | // message: '不能大于 5MB!', 66 | // }); 67 | // return Promise.reject(); 68 | // } 69 | return Promise.resolve(file); 70 | }} 71 | customRequest={(options: any) => { 72 | const { onSuccess, file } = options; 73 | file.status = 'done'; 74 | onSuccess(file.uid); 75 | getBase64(file as UploadFile, url => { 76 | setLoading(false); 77 | setImageUrl(url); 78 | }); 79 | }} 80 | listType="picture-circle" 81 | onChange={({ file }) => { 82 | file.status = 'done'; 83 | }} 84 | showUploadList={false} 85 | > 86 | {imageUrl ? ( 87 | 94 | ) : ( 95 | uploadButton 96 | )} 97 | 98 | 99 | 104 | 105 | 106 | 112 | 113 | 114 | {/* 115 | 116 | */} 117 | 118 | 119 | {t('UserInfoClient.index.wanCheng')} 120 | 121 | 122 | 123 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | export default UserInfoClient; 130 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/user-info/UserInfoClient/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | userInfo: { 5 | 'height': '100%', 6 | 'width': '100%', 7 | 'backgroundColor': token.colorBgLayout, 8 | 'position': 'relative', 9 | '& > div': { 10 | position: 'relative', 11 | overflow: 'hidden', 12 | height: '100%', 13 | paddingBottom: '40px', 14 | paddingTop: '64px', 15 | }, 16 | }, 17 | sub: { 18 | width: '100%', 19 | }, 20 | avatarImg: { 21 | borderRadius: '50%', 22 | }, 23 | content: { 24 | 'paddingTop': 16, 25 | 'paddingBottom': 42, 26 | '.ant-form': { 27 | width: 600, 28 | }, 29 | '.ant-upload-wrapper': { 30 | textAlign: 'center', 31 | position: 'relative', 32 | }, 33 | '.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload.ant-upload-select': { 34 | overflow: 'hidden', 35 | border: 'unset', 36 | }, 37 | '.ant-input-lg': { 38 | borderRadius: token.borderRadius, 39 | padding: '8px 11px', 40 | }, 41 | }, 42 | uploadText: { 43 | 'border': 0, 44 | 'background': 'none', 45 | '& > div': { 46 | marginTop: 4, 47 | }, 48 | }, 49 | })); 50 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/user-info/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, memo } from 'react'; 2 | 3 | const Layout = memo(({ children }) => { 4 | return <>{children}>; 5 | }); 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /src/app/[locale]/setting/user-info/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import React from 'react'; 3 | 4 | import UserInfoClient from './UserInfoClient'; 5 | 6 | export async function generateMetadata({ params: { locale } }: { params: { locale: string } }) { 7 | const t = await getTranslations({ locale, namespace: 'BtnList' }); 8 | return { 9 | title: t('index.geRenZiLiao'), 10 | }; 11 | } 12 | 13 | export default async function UserinfoPage() { 14 | const props = {}; 15 | return ( 16 | <> 17 | 18 | > 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/[locale]/test/(component)/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { usePathname } from 'next/navigation'; 5 | 6 | import { useStyles } from './styles'; 7 | 8 | export default function Text() { 9 | const pathname = usePathname(); 10 | const { styles } = useStyles(); 11 | return ( 12 | 13 | 14 | 15 | back to Home 16 | 17 | 18 | 19 | Pathname: {pathname} 20 | 21 | 22 | go to Inner 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/[locale]/test/(component)/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | pathname: { 5 | marginTop: '20px', 6 | lineHeight: token.testHeight, 7 | color: token.colorPrimaryTest, 8 | }, 9 | link: { 10 | color: token.colorPrimary, 11 | fontWeight: 'bold', 12 | }, 13 | container: { 14 | color: token.yellow7, 15 | textAlign: 'center', 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /src/app/[locale]/test/inner/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from 'antd'; 4 | import Link from 'next/link'; 5 | import { usePathname } from 'next/navigation'; 6 | 7 | import { useStyles } from './styles'; 8 | 9 | export default function Text() { 10 | const pathname = usePathname(); 11 | const { styles } = useStyles(); 12 | return ( 13 | 14 | 15 | back to Test 16 | 17 | 18 | back to Test, test a 链接中的文字 19 | 20 | 21 | Pathname: {pathname} 22 | 23 | 24 | line 2 Pathname: {pathname} 文字的力量 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/[locale]/test/inner/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import React from 'react'; 3 | 4 | export const metadata: Metadata = { 5 | title: 'Text / Inner', 6 | description: 'Text Inner Page', 7 | }; 8 | 9 | export default function InnerLayout({ children }: { children: React.ReactNode }) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/[locale]/test/inner/page.tsx: -------------------------------------------------------------------------------- 1 | import Index from './'; 2 | 3 | export default function Text() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/[locale]/test/inner/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | container: { 5 | color: token.yellow10, 6 | textAlign: 'center', 7 | div: { 8 | marginBottom: 26, 9 | }, 10 | }, 11 | })); 12 | -------------------------------------------------------------------------------- /src/app/[locale]/test/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | import { isMobileDevice } from '@/utils'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Test Page', 7 | description: 'Test Page', 8 | }; 9 | 10 | export default function TestPageLayout({ children }: { children: React.ReactNode }) { 11 | const mobile = isMobileDevice(); 12 | return ( 13 | 21 | Device is mobile ? {mobile ? 'yes' : 'no'} 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/[locale]/test/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SettingOutlined } from '@ant-design/icons'; 4 | import { ActionIcon } from '@lobehub/ui'; 5 | import { Button, List, Space, Spin } from 'antd'; 6 | import { Settings } from 'lucide-react'; 7 | import Image from 'next/image'; 8 | import React from 'react'; 9 | 10 | import LogoSvg from '@/../public/svg/logo.svg'; 11 | import { useAxiosRequest } from '@/utils/axios'; 12 | 13 | import TextComponent from './(component)'; 14 | import { useStyles } from './styles'; 15 | 16 | const app_name = 'bjwswang'; 17 | const app_namespace = 'rag-eval'; 18 | 19 | const Test = React.memo(() => { 20 | const { styles } = useStyles(); 21 | const body = { 22 | data: { 23 | // app_name, 24 | // app_namespace, 25 | }, 26 | }; 27 | // 如果需要刷新页面就自动调用接口, 需要用封装后的 useAxiosRequest, 避免 axios hooks 初始化前就调用 28 | const [{ data, loading, error }, loadList] = useAxiosRequest( 29 | { 30 | url: '/kubeagi-apis/gpts/chat/conversations', 31 | method: 'POST', 32 | }, 33 | {}, // use axios options 34 | body // 可选: loadList 回调参数 35 | ); 36 | 37 | // 如果执行某 action, 比如创建可以直接使用 useAxios 38 | // const [{ data, loading, error }, excute] = useAxios( 39 | // { 40 | // url: '/kubeagi-apis/xxxx', 41 | // method: 'POST', 42 | // }, 43 | // { 44 | // manual: true, 45 | // }, 46 | // ); 47 | // ... 48 | // somewhere: excute([config[, options]]) 49 | // 推荐使用 useAxios / useAxiosRequest, 也可以直接使用 axios 的 instance: 50 | // const axios = createCustomAxios() 51 | 52 | const [{ data: get_test_data, loading: get_test_loading, error: get_test_error }, loadStat] = 53 | useAxiosRequest({ 54 | url: '/kubeagi-apis/bff/model/files/chunks?fileName=abc.csv&bucket=arcadia&bucketPath=dataset/dataset/v1&etag=aaaaaaa&md5=bbbbb', 55 | headers: { 56 | namespace: 'arcadia', 57 | }, 58 | }); 59 | 60 | return ( 61 | 62 | 63 | 64 | { 66 | loadList(body); 67 | loadStat(); 68 | }} 69 | > 70 | 刷新 71 | 72 | 73 | 79 | 80 | hooks (axios.get) test: 81 | 82 | {get_test_data ? ( 83 | <>res: {JSON.stringify(get_test_data)}> 84 | ) : ( 85 | <>error: {JSON.stringify(get_test_error)}> 86 | )} 87 | 88 | 89 | 90 | 91 | 92 | hooks (axios.post) conversations:{' '} 93 | {`\n(app_name: ${app_name}, app_namespace: ${app_namespace})`} 94 | 95 | {item?.messages?.[0]?.query}} 99 | /> 100 | 101 | {error ? {JSON.stringify(error)} : null} 102 | 103 | 104 | 105 | Get started by editing 106 | src/app/page.tsx 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | ); 125 | }); 126 | 127 | export default Test; 128 | -------------------------------------------------------------------------------- /src/app/[locale]/test/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ css }) => { 4 | return { 5 | wrapper: { 6 | 'display': 'flex', 7 | 'flexDirection': 'column', 8 | 'alignItems': 'center', 9 | 'justifyContent': 'space-between', 10 | 11 | 'minHeight': '60vh', 12 | 'padding': '2rem', 13 | '.ant-spin-nested-loading': { 14 | 'width': '100%', 15 | '.ant-list': { 16 | backgroundColor: 'white', 17 | margin: '16px auto', 18 | width: 500, 19 | }, 20 | }, 21 | }, 22 | logo: { 23 | // filter: 'invert(1) drop-shadow(0 0 0.3rem #ffffff70)' 24 | position: 'relative', 25 | }, 26 | description: { 27 | display: 'inherit', 28 | justifyContent: 'inherit', 29 | alignItems: 'inherit', 30 | fontSize: '0.85rem', 31 | width: '100%', 32 | zIndex: 2, 33 | maxWidth: 'var(--max-width)', 34 | a: { 35 | display: 'flex', 36 | justifyContent: 'center', 37 | alignItems: 'center', 38 | gap: '0.5rem', 39 | }, 40 | p: { 41 | position: 'relative', 42 | margin: 0, 43 | padding: '16px', 44 | backgroundColor: 'rgba(var(--callout-rgb), 0.5)', 45 | border: '1px solid rgba(var(--callout-border-rgb), 0.3)', 46 | borderRadius: 'var(--border-radius)', 47 | }, 48 | }, 49 | 50 | code: css` 51 | font-weight: 700; 52 | `, 53 | 54 | center: css` 55 | position: relative; 56 | 57 | display: flex; 58 | align-items: center; 59 | justify-content: center; 60 | 61 | padding: 4rem 0; 62 | &::before { 63 | width: 480px; 64 | height: 360px; 65 | margin-left: -400px; 66 | 67 | background: var(--secondary-glow); 68 | border-radius: 50%; 69 | } 70 | 71 | &::after { 72 | z-index: -1; 73 | width: 240px; 74 | height: 180px; 75 | background: var(--primary-glow); 76 | } 77 | 78 | &::before, 79 | &::after { 80 | content: ''; 81 | 82 | position: absolute; 83 | left: 50%; 84 | transform: translateZ(0); 85 | 86 | filter: blur(45px); 87 | } 88 | `, 89 | }; 90 | }); 91 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubeagi/agent-portal/33a0f0bd701bd13b4df352c5d3abe3d2e33ed82e/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | // Since we have a `not-found.tsx` page on the root, a layout file 8 | // is required, even if it's just passing children through. 9 | export default function RootLayout({ children }: Props) { 10 | return <>{children}>; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default () => { 4 | return <>>; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/BtnsBlock/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ActionIcon } from '@lobehub/ui'; 4 | import { Button, Flex } from 'antd'; 5 | import { createStyles } from 'antd-style'; 6 | import classNames from 'classnames'; 7 | import { ChevronRight } from 'lucide-react'; 8 | import { useTranslations } from 'next-intl'; 9 | import Link from 'next/link'; 10 | import React from 'react'; 11 | 12 | export const useStyles = createStyles(({ token }) => ({ 13 | btns: { 14 | marginBottom: 16, 15 | borderRadius: 16, 16 | maxHeight: '100%', 17 | overflowY: 'auto', 18 | width: '100%', 19 | }, 20 | linkWrapper: { 21 | 'color': 'inherit', 22 | '&:hover': { 23 | color: 'inherit', 24 | }, 25 | }, 26 | btn: { 27 | 'width': '100%', 28 | 'backgroundColor': token.colorBgBase, 29 | 'paddingLeft': '16px', 30 | 'cursor': 'pointer', 31 | '&:hover': { 32 | backgroundColor: token.controlItemBgHover, 33 | animation: 'inactivelink-hover-animation 150ms ease forwards', 34 | // content: { 35 | // boxSadow: 'unset', 36 | // } 37 | }, 38 | }, 39 | content: { 40 | flex: '1 1', 41 | padding: '12px 12px 12px 0', 42 | minHeight: '56px', 43 | boxShadow: '0 -1px 0 rgba(0, 0 ,0 ,.05)', 44 | }, 45 | content_left: { 46 | 'flex': '1 1', 47 | 'fontSize': 16, 48 | 'fontWeight': '500', 49 | 'lineHeight': '22px', 50 | 'textAlign': 'left', 51 | 'paddingLeft': 'unset', 52 | '&:hover': { 53 | backgroundColor: 'inherit !important', 54 | }, 55 | '&:hover.ant-btn-dangerous': { 56 | color: `${token.colorError} !important`, 57 | }, 58 | }, 59 | content_right: { 60 | marginLeft: '8px', 61 | }, 62 | icon: { 63 | marginRight: 16, 64 | borderRadius: '10px', 65 | svg: { 66 | width: '20px', 67 | height: '20px', 68 | }, 69 | }, 70 | extra: { 71 | color: token.colorTextTertiary, 72 | fontSize: token.fontSize, 73 | transform: 'translateY(-16px)', 74 | }, 75 | btn_extra: { 76 | color: token.colorTextTertiary, 77 | }, 78 | })); 79 | 80 | export interface Btn { 81 | icon?: any; 82 | title: string; 83 | onClick?: () => void; 84 | btn_extra?: React.ReactNode | string; // extra + onClick 显示箭头 85 | href?: string; // href 显示箭头 86 | danger?: boolean; 87 | action?: React.ReactNode | string; // action 和 href 二选一 88 | icon_bg?: string; 89 | } 90 | 91 | export interface BtnsBlockProps { 92 | btns: Btn[]; 93 | extra?: string; 94 | } 95 | 96 | const BtnsBlock = React.memo(props => { 97 | const { styles, theme } = useStyles(); 98 | const { btns, extra } = props; 99 | const t = useTranslations('BtnsBlock'); 100 | return ( 101 | <> 102 | 103 | {btns.map((item, idx) => { 104 | const icon = item.icon; 105 | const icon_bg = item.icon_bg || theme.colorPrimary; 106 | const key = item.title + idx; 107 | const children = ( 108 | 109 | {icon ? ( 110 | 111 | 112 | 113 | ) : null} 114 | 115 | 116 | {item.title || t('index.moRenBiaoTi')} 117 | 118 | {item.btn_extra ? ( 119 | 123 | {item.btn_extra} 124 | 125 | ) : null} 126 | {item.href || item.btn_extra ? ( 127 | 128 | 129 | 130 | ) : null} 131 | {item.action ? ( 132 | 133 | {item.action} 134 | 135 | ) : null} 136 | 137 | 138 | ); 139 | if (item.href) { 140 | return ( 141 | 142 | {children} 143 | 144 | ); 145 | } 146 | return children; 147 | })} 148 | 149 | {extra ? {extra} : null} 150 | > 151 | ); 152 | }); 153 | 154 | export default BtnsBlock; 155 | -------------------------------------------------------------------------------- /src/components/EmptyPage/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createStyles } from 'antd-style'; 4 | import React from 'react'; 5 | 6 | export const useStyles = createStyles(({ token }) => ({ 7 | wrapper: { 8 | height: '100%', 9 | position: 'relative', 10 | width: '100%', 11 | backgroundColor: token.colorBgBase, 12 | }, 13 | })); 14 | 15 | export default function EmptyPage({ children }: { children?: React.ReactNode }) { 16 | const { styles } = useStyles(); 17 | return {children}; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Spin } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import React from 'react'; 6 | import { useSelector } from 'react-redux'; 7 | 8 | export const useStyles = createStyles(({ token }) => ({ 9 | wrapper: { 10 | 'height': '100%', 11 | 'position': 'relative', 12 | 'width': '100%', 13 | 'backgroundColor': token.colorBgBase, 14 | '.ant-spin-nested-loading': { 15 | height: '100%', 16 | }, 17 | '.ant-spin': { 18 | left: '50% !important', 19 | position: 'absolute', 20 | top: '50% !important', 21 | transform: 'translate(-50%, -50%)', 22 | }, 23 | }, 24 | })); 25 | 26 | export default function Loading({ 27 | children, 28 | loading, 29 | }: { 30 | children?: React.ReactNode; 31 | loading?: boolean; 32 | }) { 33 | const { styles } = useStyles(); 34 | const pageLoading = useSelector((store: any) => store.pageLoading); 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ReturnBtn/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ActionIcon } from '@lobehub/ui'; 4 | import { Flex } from 'antd'; 5 | import { createStyles } from 'antd-style'; 6 | import { ChevronLeft } from 'lucide-react'; 7 | import Link from 'next/link'; 8 | import React from 'react'; 9 | 10 | export const useStyles = createStyles(() => ({ 11 | returnBtn: { 12 | padding: '0 16px', 13 | height: '64px', 14 | position: 'absolute', 15 | top: 0, 16 | width: '100%', 17 | zIndex: 9, 18 | overflowY: 'auto', 19 | }, 20 | btn: { 21 | borderRadius: '12px !important', 22 | svg: { 23 | width: 25, 24 | height: 25, 25 | }, 26 | }, 27 | title: { 28 | flex: '1 1', 29 | alignItems: 'center', 30 | display: 'flex', 31 | fontWeight: 590, 32 | fontSize: 16, 33 | flexDirection: 'column', 34 | marginLeft: '-40px', 35 | }, 36 | leftTitle: { 37 | fontWeight: 590, 38 | fontSize: 16, 39 | }, 40 | layout: { 41 | width: '100%', 42 | }, 43 | })); 44 | 45 | interface ReturnBtnProps { 46 | to?: string; 47 | title?: string; 48 | isLeftTitle?: boolean; 49 | extra?: React.ReactNode; 50 | } 51 | 52 | const ReturnBtn = React.memo(props => { 53 | const { to, extra, isLeftTitle } = props; 54 | const { styles } = useStyles(); 55 | return ( 56 | 57 | 58 | 59 | 60 | {isLeftTitle ? ( 61 | 62 | {props.title} 63 | {extra} 64 | 65 | ) : ( 66 | {props.title} 67 | )} 68 | 69 | ); 70 | }); 71 | 72 | export default ReturnBtn; 73 | -------------------------------------------------------------------------------- /src/components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Flex } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import React from 'react'; 6 | 7 | export const useStyles = createStyles(() => ({ 8 | TitleCom: { 9 | padding: '0 24px', 10 | height: '64px', 11 | position: 'absolute', 12 | top: 0, 13 | width: '100%', 14 | zIndex: 9, 15 | overflowY: 'auto', 16 | }, 17 | title: { 18 | flex: '1 1', 19 | alignItems: 'center', 20 | display: 'flex', 21 | fontWeight: 590, 22 | fontSize: 16, 23 | flexDirection: 'column', 24 | marginLeft: '-40px', 25 | }, 26 | leftTitle: { 27 | fontWeight: 590, 28 | fontSize: 16, 29 | }, 30 | layout: { 31 | width: '100%', 32 | }, 33 | })); 34 | 35 | interface TitleComProps { 36 | to?: string; 37 | title?: string; 38 | isLeftTitle?: boolean; 39 | extra?: React.ReactNode; 40 | } 41 | 42 | const TitleCom = React.memo(props => { 43 | const { extra, isLeftTitle } = props; 44 | const { styles } = useStyles(); 45 | return ( 46 | 47 | {isLeftTitle ? ( 48 | 49 | {props.title} 50 | {extra} 51 | 52 | ) : ( 53 | {props.title} 54 | )} 55 | 56 | ); 57 | }); 58 | 59 | export default TitleCom; 60 | -------------------------------------------------------------------------------- /src/config/oidc.mjs: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | server: { 4 | url: process.env.OIDC_SERVER_URL, 5 | }, 6 | client: { 7 | client_id: process.env.CLIENT_ID, 8 | client_secret: process.env.CLIENT_SECRET, 9 | redirect_uri: '/oidc/callback', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | import { notFound } from 'next/navigation'; 3 | 4 | // Can be imported from a shared config 5 | export const locales = ['en', 'zh']; 6 | 7 | export default getRequestConfig(async ({ locale }) => { 8 | // Validate that the incoming `locale` parameter is valid 9 | if (!locales.includes(locale as any)) notFound(); 10 | const messages = await import(`../messages/${locale}`); 11 | return { 12 | messages: messages.default, 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/Chats/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Divider, List, Skeleton } from 'antd'; 4 | import classnames from 'classnames'; 5 | import cloneDeep from 'lodash/cloneDeep'; 6 | import React from 'react'; 7 | import InfiniteScroll from 'react-infinite-scroll-component'; 8 | 9 | import { useAxiosRequest } from '@/utils/axios'; 10 | 11 | // import { DEFAULT_CHAT } from '@/utils/constants'; 12 | import ChatItem from './Item'; 13 | import { useStyles } from './styles'; 14 | 15 | const default_load_size = 10; 16 | 17 | // const default_chat_obj: any = { 18 | // id: DEFAULT_CHAT, 19 | // title: I18N.Chats.index.moRenDuiHua, 20 | // desc: I18N.Chats.index.duiHuaBaLaBa, 21 | // }; 22 | 23 | const EMPTY_ITEMS = [1, 2, 3, 4]; 24 | 25 | const Chats: any = () => { 26 | const { styles } = useStyles(); 27 | const [allData, setAllData] = React.useState([]); 28 | const [showData, setShowData] = React.useState([]); 29 | 30 | const [{ data }, getList] = useAxiosRequest({ 31 | url: '/kubeagi-apis/gpts/chat/conversations', 32 | method: 'POST', 33 | }); 34 | 35 | React.useEffect(() => { 36 | const _all = 37 | data?.map((item: any) => ({ 38 | title: item.messages?.[0]?.query || item.app_name, 39 | desc: item.messages?.[0]?.answer, 40 | ...item, 41 | })) || []; 42 | setAllData(_all); 43 | setShowData( 44 | cloneDeep(_all).splice( 45 | 0, 46 | default_load_size * (Math.ceil(document.body.clientHeight / 1000) || 2) 47 | ) 48 | ); 49 | }, [data]); 50 | 51 | const loadMoreData = React.useCallback(() => { 52 | if (allData.length === showData.length) return; // 删除最后一条会调用一次 next, 暂时用此屏蔽再次 setData 53 | setShowData((pre: any) => [ 54 | ...pre, 55 | ...cloneDeep(allData).splice(pre.length, default_load_size), 56 | ]); 57 | }, [allData, showData]); 58 | 59 | const delDom = React.useCallback( 60 | (record?: any, isActiveChat?: boolean) => { 61 | setShowData((pre: any) => pre.filter((_item: any) => _item.id !== record.id)); 62 | setAllData((pre: any) => pre.filter((_item: any) => _item.id !== record.id)); 63 | if (isActiveChat) { 64 | document.querySelector(`.${styles.chats}`)?.scrollTo({ 65 | top: 0, 66 | behavior: 'smooth', 67 | }); 68 | } 69 | }, 70 | [setShowData, setAllData] 71 | ); 72 | 73 | const renderEmpty = React.useCallback(() => { 74 | return EMPTY_ITEMS.map(item => { 75 | return ( 76 | 85 | ); 86 | }); 87 | }, []); 88 | return ( 89 | 90 | 91 | {data && showData.length > 0 ? ( 92 | default_load_size ? ( 97 | 98 | 我是有底线的 99 | 100 | ) : null 101 | } 102 | hasMore={showData.length < allData.length} 103 | loader={ 104 | 113 | } 114 | next={loadMoreData} 115 | scrollableTarget={'infinite_' + styles.chats} 116 | > 117 | ( 121 | 122 | { 127 | getList(); 128 | }} 129 | wrapperClass={styles.chats} 130 | /> 131 | 132 | )} 133 | /> 134 | 135 | ) : ( 136 | renderEmpty() 137 | )} 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default Chats; 144 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/Chats/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | chats: { 5 | position: 'relative', 6 | flex: '1 1 0%', 7 | }, 8 | content: { 9 | 'paddingLeft': 8, 10 | 'paddingBottom': 16, 11 | '.ant-typography': { 12 | marginBottom: 'unset', 13 | }, 14 | '.ant-list-item': { 15 | padding: 0, 16 | borderBlockEnd: 'unset', 17 | marginBottom: '1px', 18 | }, 19 | }, 20 | emptyItem: { 21 | 'padding': '16px 16px 0 12px', 22 | '.ant-skeleton-header': { 23 | paddingInlineEnd: '8px', 24 | }, 25 | }, 26 | dividerText: { 27 | color: `${token.colorTextDescription} !important`, 28 | }, 29 | scroll: { 30 | paddingBottom: 70, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/SideBarHeader/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlusCircleFilled } from '@ant-design/icons'; 4 | import { Flex } from 'antd'; 5 | import { ChevronRight } from 'lucide-react'; 6 | import { useTranslations } from 'next-intl'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | import React from 'react'; 10 | 11 | import Logo from '@/../public/svg/logo.svg'; 12 | 13 | import { useStyles } from './styles'; 14 | 15 | const SidebarHeader = () => { 16 | const t = useTranslations('SideBarHeader'); 17 | const { styles } = useStyles(); 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {t('index.chuangJianZhiNengTi')} 29 | 30 | 31 | 32 | 33 | {t('index.faXianZhiNengTi')} 34 | 35 | 36 | 37 | 38 | {t('index.duiHua')} 39 | {/* 40 | {t('index.xinDuiHua')} 41 | */} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default SidebarHeader; 48 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/SideBarHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebarHeader: { 5 | overflow: 'hidden', 6 | backgroundColor: token.colorBgBase, 7 | }, 8 | btns: { 9 | 'flex': '1 1 0%', 10 | 'padding': 8, 11 | 'borderBottom': `1px solid ${token.colorSplit}`, 12 | '.anticon': { 13 | color: token.colorPrimary, 14 | }, 15 | }, 16 | btnName: { 17 | flex: '1 1 0%', 18 | textOverflow: 'ellipsis', 19 | overflow: 'hidden', 20 | whiteSpace: 'nowrap', 21 | }, 22 | linkItem: { 23 | 'padding': '8px 12px', 24 | 'borderRadius': '12px', 25 | 'display': 'flex', 26 | 'alignItems': 'center', 27 | 'gap': '12px', 28 | 'color': token.colorText, 29 | 'fontWeight': 600, 30 | 'fontSize': 16, 31 | '&:hover': { 32 | translate: 'all .15s', 33 | color: token.colorText, 34 | backgroundColor: token.controlItemBgHover, 35 | }, 36 | '.anticon': { 37 | fontSize: 42, 38 | color: token.colorPrimary, 39 | }, 40 | }, 41 | logo: { 42 | height: '50px', 43 | lineHeight: '50px', 44 | borderBottom: `1px solid ${token.colorSplit}`, 45 | color: token.colorPrimary, 46 | padding: '0 20px', 47 | fontSize: '24px', 48 | cursor: 'pointer', 49 | a: { 50 | verticalAlign: 'sub', 51 | }, 52 | svg: { 53 | fill: token.colorTextBase, 54 | }, 55 | }, 56 | chatsTitle: { 57 | padding: '8px 8px 8px 20px', 58 | }, 59 | _title: { 60 | fontSize: 16, 61 | fontWeight: 600, 62 | }, 63 | newbtn: { 64 | 'display': 'flex', 65 | 'alignItems': 'center', 66 | 'cursor': 'pointer', 67 | 'color': token.colorPrimary, 68 | 'padding': '8px 12px', 69 | 'borderRadius': '12px', 70 | '&:hover': { 71 | translate: 'all .15s', 72 | backgroundColor: token.controlItemBgHover, 73 | }, 74 | 'svg': { 75 | marginRight: 8, 76 | width: 20, 77 | }, 78 | }, 79 | })); 80 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/UserInfoBottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { UserOutlined } from '@ant-design/icons'; 4 | import { ActionIcon } from '@lobehub/ui'; 5 | import { sdk } from '@yuntijs/bff-client'; 6 | import { Avatar, Dropdown, Skeleton, Space, Typography } from 'antd'; 7 | import { createStyles } from 'antd-style'; 8 | import { Check, Monitor, Moon, Settings, Sun } from 'lucide-react'; 9 | import { useTranslations } from 'next-intl'; 10 | import Image from 'next/image'; 11 | import { useRouter } from 'next/navigation'; 12 | import React from 'react'; 13 | import { Flexbox } from 'react-layout-kit'; 14 | import { useDispatch, useSelector } from 'react-redux'; 15 | 16 | const { Text } = Typography; 17 | 18 | export const useStyles = createStyles(({ token }) => { 19 | const defaultHeight = '60px'; 20 | return { 21 | userinfo: { 22 | lineHeight: defaultHeight, 23 | height: defaultHeight, 24 | width: '100%', 25 | padding: '0 12px', 26 | overflowY: 'hidden', 27 | borderTop: `1px solid ${token.colorSplit}`, 28 | alignItems: 'center', 29 | display: 'flex', 30 | }, 31 | skeletonUserinfo: { 32 | 'height': defaultHeight, 33 | 'display': 'flex', 34 | 'alignItems': 'center', 35 | 'paddingTop': '15px', 36 | '.ant-skeleton': { 37 | 'height': '40px', 38 | 'paddingLeft': '12px', 39 | 'display': 'block', 40 | '.ant-skeleton-header': { 41 | paddingInlineEnd: '12px', 42 | }, 43 | '.ant-skeleton-paragraph': { 44 | marginBottom: 0, 45 | paddingTop: '7px', 46 | }, 47 | }, 48 | }, 49 | checkIcon: { 50 | marginRight: 8, 51 | verticalAlign: 'bottom', 52 | width: '15px', 53 | }, 54 | hideIcon: { 55 | marginRight: 8, 56 | verticalAlign: 'bottom', 57 | width: '15px', 58 | color: 'rgba(0, 0, 0, 0)', 59 | }, 60 | icons: { 61 | textAlign: 'right', 62 | lineHeight: defaultHeight, 63 | paddingTop: '10px', 64 | }, 65 | avator: { 66 | minWidth: '38px', 67 | }, 68 | username: { 69 | width: 'calc(100% - 80px)', 70 | cursor: 'pointer', 71 | }, 72 | hover: { 73 | 'height': '40px', 74 | 'lineHeight': '40px', 75 | 'padding': '0 12px', 76 | '.ant-typography': { 77 | lineHeight: '40px', 78 | maxWidth: 'calc(100% - 40px)', 79 | overflow: 'hidden', 80 | textOverflow: 'ellipsis', 81 | }, 82 | '&:hover': { 83 | color: 'rgba(0, 0, 0, 0.88)', 84 | backgroundColor: token.controlItemBgHover, 85 | borderRadius: '12px', 86 | }, 87 | }, 88 | }; 89 | }); 90 | 91 | export default function UserInfoBottom() { 92 | const t = useTranslations('SideBar'); 93 | const dispatch = useDispatch(); 94 | const { styles } = useStyles(); 95 | const theme = useSelector((store: any) => store.theme); 96 | const router = useRouter(); 97 | const { data } = sdk.useGetCurrentUser(); 98 | const user = data?.userCurrent; 99 | const getTitle = (type: string, text: string) => { 100 | if (type === theme) { 101 | return ( 102 | 103 | 104 | {text} 105 | 106 | ); 107 | } 108 | return ( 109 | 110 | 111 | {text} 112 | 113 | ); 114 | }; 115 | const items = [ 116 | { 117 | key: 'light', 118 | label: getTitle('light', t('UserInfoBottom.liangSeMoShi')), 119 | }, 120 | { 121 | key: 'dark', 122 | label: getTitle('dark', t('UserInfoBottom.heiAnMoShi')), 123 | }, 124 | { 125 | key: 'auto', 126 | label: getTitle('auto', t('UserInfoBottom.genSuiXiTong')), 127 | }, 128 | ]; 129 | return ( 130 | 131 | 132 | {user?.name ? ( 133 | 134 | 135 | 139 | ) : ( 140 | 141 | ) 142 | } 143 | size={28} 144 | /> 145 | 146 | {user?.name} 147 | 148 | ) : ( 149 | 150 | 151 | 152 | )} 153 | 154 | 155 | 156 | { 160 | if (theme === key) return; 161 | dispatch({ 162 | type: 'TRIGGER_THEME', 163 | theme: key, 164 | }); 165 | }, 166 | }} 167 | placement="top" 168 | > 169 | { 171 | if (theme === 'auto') { 172 | return Monitor; 173 | } 174 | return theme === 'light' ? Sun : Moon; 175 | })()} 176 | onClick={() => { 177 | if (theme === 'auto') return; 178 | dispatch({ 179 | type: 'TRIGGER_THEME', 180 | theme: theme === 'light' ? 'dark' : 'light', 181 | }); 182 | }} 183 | style={{ 184 | borderRadius: '12px', 185 | }} 186 | // title={`Switch to ${theme === 'light' ? 'Dark' : 'Light'}`} 187 | /> 188 | 189 | { 192 | router.push('/setting'); 193 | }} 194 | style={{ 195 | borderRadius: '12px', 196 | }} 197 | title={t('UserInfoBottom.sheZhi')} 198 | /> 199 | {/* */} 203 | 204 | 205 | 206 | ); 207 | } 208 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/index.draggable.panel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui'; 4 | import { createStyles } from 'antd-style'; 5 | import { useTranslations } from 'next-intl'; 6 | import React from 'react'; 7 | 8 | import UserInfoBottom from './UserInfoBottom'; 9 | 10 | const useStyles = createStyles(({ css, token }) => ({ 11 | content: css` 12 | display: flex; 13 | flex-direction: column; 14 | `, 15 | drawer: { 16 | // backgroundColor: token.colorBgLayout, 17 | position: 'relative', 18 | }, 19 | header: css` 20 | border-bottom: 1px solid ${token.colorBorder}; 21 | `, 22 | part: css` 23 | margin-bottom: 30vh; 24 | `, 25 | })); 26 | 27 | const ChatList = () => { 28 | const { styles } = useStyles(); 29 | const t = useTranslations('SideBar'); 30 | 31 | return ( 32 | 42 | 51 | {t('index.zhiNengTiLieBiao')} 52 | {t('index.duiHuaLieBiao')} 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default ChatList; 60 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import classNames from 'classnames'; 4 | import { usePathname } from 'next/navigation'; 5 | import React from 'react'; 6 | 7 | import { locales } from '@/i18n'; 8 | 9 | import Chats from './Chats'; 10 | import SideBarHeader from './SideBarHeader'; 11 | import UserInfoBottom from './UserInfoBottom'; 12 | import { useStyles } from './styles'; 13 | 14 | export default function SideBar() { 15 | const pathname: any = usePathname(); 16 | const { styles } = useStyles(); 17 | const is_no_sidebar_route = new RegExp( 18 | `^(?:\/(?:${locales.join('|')}))?\/oidc(?:\/.*|\/?|)|\/logout(?:\/.*|\/?)$` 19 | ).test(pathname); 20 | if (is_no_sidebar_route) return <>>; 21 | const showSidebar = ['/chat'].includes(pathname); 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/AppLayout/SideBar/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebar: { 5 | 'position': 'relative', 6 | 'display': 'flex', 7 | 'flexDirection': 'column', 8 | 'flexShrink': '0', 9 | 'width': '336px', 10 | 'height': '100%', 11 | 'backgroundColor': token.colorBgBase, 12 | // 处理 [dir='ltr'] 和 [dir='rtl'] 13 | 'html[dir="ltr"] &': { 14 | borderRight: `1px solid ${token.colorSplit}`, 15 | }, 16 | 'html[dir="rtl"] &': { 17 | borderLeft: `1px solid ${token.colorSplit}`, 18 | }, 19 | 20 | // 媒体查询 21 | '@media (max-width: 879px)': { 22 | '&[sidebar-visible="false"]': { 23 | 'boxShadow': 'none', 24 | 25 | '& button': { 26 | opacity: 0, 27 | }, 28 | }, 29 | 30 | 'html[dir="ltr"] &': { 31 | left: 0, 32 | borderRight: '1px solid', 33 | zIndex: 9, 34 | }, 35 | 36 | 'html[dir="rtl"] &': { 37 | right: 0, 38 | borderLeft: '1px solid', 39 | zIndex: 9, 40 | }, 41 | 42 | '&': { 43 | 'position': 'absolute', 44 | 'width': '100%', 45 | 46 | '&.need_hide_sidebar': { 47 | display: 'none', 48 | }, 49 | }, 50 | 51 | '[dir] &': { 52 | border: 'transparent', 53 | }, 54 | 55 | '[sidebar-visible="false"]': { 56 | '[dir="ltr"] &': { 57 | transform: 'translate(-200%)', 58 | }, 59 | '[dir="rtl"] &': { 60 | transform: 'translate(200%)', 61 | }, 62 | }, 63 | }, 64 | }, 65 | })); 66 | -------------------------------------------------------------------------------- /src/layout/AppLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from 'antd'; 2 | 3 | import { User } from '@/types/user'; 4 | 5 | import SideBar from './SideBar'; 6 | 7 | export default function AppLayout({ children }: { children: React.ReactNode; user?: User }) { 8 | return ( 9 | 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/layout/AppLayoutTemplate/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import AppLayout from '@/layout/AppLayout'; 6 | import AppNoAuthLayout from '@/layout/AppNoAuthLayout'; 7 | import AppSkeletonLayout from '@/layout/AppSkeletonLayout'; 8 | import { useAuthContext } from '@/layout/AuthLayout'; 9 | 10 | interface Props { 11 | children?: React.ReactNode; 12 | } 13 | 14 | const AppLayoutTemplate = React.memo(({ children }) => { 15 | const { authed } = useAuthContext() || { authed: undefined }; 16 | if (authed === undefined) { 17 | return {children}; 18 | } 19 | const App = authed ? AppLayout : AppNoAuthLayout; 20 | return {children}; 21 | }); 22 | 23 | export default AppLayoutTemplate; 24 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/Chats/Item.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Flex, Typography } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import classNames from 'classnames'; 6 | import Image from 'next/image'; 7 | import Link from 'next/link'; 8 | import { useParams } from 'next/navigation'; 9 | import React from 'react'; 10 | 11 | export const useStyles = createStyles(({ token }) => { 12 | const activeStyle = { 13 | color: token.colorTextBase, 14 | backgroundColor: token.controlItemBgHover, 15 | transition: 'all .15s', 16 | }; 17 | return { 18 | chatItem: { 19 | 'alignItems': 'center', 20 | 'borderRadius': '16px', 21 | 'display': 'flex', 22 | 'flexShrink': '0', 23 | 'height': '74px', 24 | 'overflow': 'visible', 25 | 'padding': '0 12px', 26 | 'position': 'relative', 27 | 'color': token.colorTextBase, 28 | 'width': '100%', 29 | '&:hover': activeStyle, 30 | '&:active': { 31 | backgroundColor: token.controlItemBgActiveDisabled, 32 | }, 33 | '&:hover .showBtn': { 34 | display: 'block', 35 | }, 36 | '&:not(:first-child)': { 37 | marginTop: 1, 38 | }, 39 | }, 40 | activeItem: { 41 | ...activeStyle, 42 | '&:active': { 43 | backgroundColor: `${token.controlItemBgHover} !important`, 44 | }, 45 | }, 46 | icon: { 47 | 'display': 'flex', 48 | '.anticon': { 49 | fontSize: 48, 50 | color: token.colorPrimary, 51 | }, 52 | }, 53 | content: { 54 | flex: '1 1 0%', 55 | display: 'flex', 56 | flexDirection: 'column', 57 | flexGrow: '1', 58 | overflow: 'hidden', 59 | }, 60 | title: { 61 | fontSize: 16, 62 | fontWeight: 600, 63 | lineHeight: '24px', 64 | }, 65 | msg: { 66 | color: token.colorTextSecondary, 67 | fontSize: 14, 68 | fontWeight: 400, 69 | lineHeight: '24px', 70 | }, 71 | itemBtn: { 72 | 'display': 'none', 73 | 'position': 'absolute', 74 | 'top': '20px', 75 | 'right': '25px', 76 | '.ant-btn': { 77 | 'padding': '4px 10px', 78 | 'border': 'unset', 79 | 'boxShadow': '0 1px 8px 0 rgba(0,0,0,.12)', 80 | '.anticon': { 81 | color: token.colorTextBase, 82 | transform: 'scale(1.5)', 83 | }, 84 | }, 85 | }, 86 | dropmenus: { 87 | '.ant-btn-link': { 88 | 'padding': 0, 89 | '.ant-btn-icon': { 90 | verticalAlign: 'text-bottom', 91 | }, 92 | }, 93 | }, 94 | }; 95 | }); 96 | 97 | interface Props { 98 | data: { 99 | id: string; 100 | title: string; 101 | desc: string; 102 | app_namespace: string; 103 | app_name: string; 104 | }; 105 | } 106 | 107 | const ChatItem: any = (props: Props) => { 108 | const { data } = props; 109 | const { styles } = useStyles(); 110 | const { locale } = useParams(); 111 | const id = 'chatItem' + data.id; 112 | return ( 113 | 114 | 122 | 123 | 124 | 125 | 126 | 127 | {data.title} 128 | 129 | 130 | {data.desc} 131 | 132 | 133 | 134 | 135 | ); 136 | }; 137 | 138 | export default ChatItem; 139 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/Chats/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import classnames from 'classnames'; 4 | import { useTranslations } from 'next-intl'; 5 | import React from 'react'; 6 | 7 | import { DEFAULT_CHAT } from '@/utils/constants'; 8 | 9 | import ChatItem from './Item'; 10 | import { useStyles } from './styles'; 11 | 12 | const Chats: any = () => { 13 | const { styles } = useStyles(); 14 | const t = useTranslations('Chats'); 15 | 16 | const default_chat_obj: any = React.useMemo(() => { 17 | return { 18 | id: DEFAULT_CHAT, 19 | title: t('index.moRenDuiHua'), 20 | desc: t('index.duiHuaBaLaBa'), 21 | }; 22 | }, []); 23 | 24 | // todo: 未登录时从后端获取默认对话 ? 25 | // const [{ data }, getList] = useAxiosRequest({ 26 | // url: '/kubeagi-apis/gpts/chat/conversations/default', // unused 27 | // method: 'POST', 28 | // }); 29 | // const list = useMemo(() => { 30 | // const res = 31 | // data?.map((item: any) => ({ 32 | // title: item.messages?.[0]?.query, 33 | // desc: item.messages?.[0]?.answer, 34 | // ...item, 35 | // })) || []; 36 | // return res; 37 | // }, [data]); 38 | 39 | return ( 40 | 41 | 42 | {[ 43 | default_chat_obj, 44 | // ...list, 45 | ].map((item: any, idx: number) => ( 46 | { 50 | // getList(); 51 | if (isActiveChat) { 52 | document.querySelector(`.${styles.chats}`)?.scrollTo({ 53 | top: 0, 54 | behavior: 'smooth', 55 | }); 56 | } 57 | }} 58 | /> 59 | ))} 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default Chats; 66 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/Chats/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(() => ({ 4 | chats: { 5 | position: 'relative', 6 | flex: '1 1 0%', 7 | }, 8 | content: { 9 | 'paddingLeft': 8, 10 | 'paddingBottom': 16, 11 | '.ant-typography': { 12 | marginBottom: 'unset', 13 | }, 14 | }, 15 | })); 16 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/SideBarHeader/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlusCircleFilled } from '@ant-design/icons'; 4 | import { Flex } from 'antd'; 5 | import { ChevronRight } from 'lucide-react'; 6 | import { useTranslations } from 'next-intl'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | import React from 'react'; 10 | 11 | import Logo from '@/../public/svg/logo.svg'; 12 | 13 | import { useStyles } from './styles'; 14 | 15 | const SidebarHeader = () => { 16 | const t = useTranslations('SideBarHeader'); 17 | const { styles } = useStyles(); 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {t('index.chuangJianZhiNengTi')} 29 | 30 | 31 | 32 | 33 | {t('index.faXianZhiNengTi')} 34 | 35 | 36 | 37 | 38 | {t('index.duiHua')} 39 | {/* 40 | {t('index.xinDuiHua')} 41 | */} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default SidebarHeader; 48 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/SideBarHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebarHeader: { 5 | overflow: 'hidden', 6 | backgroundColor: token.colorBgBase, 7 | }, 8 | btns: { 9 | 'flex': '1 1 0%', 10 | 'padding': 8, 11 | 'borderBottom': `1px solid ${token.colorSplit}`, 12 | '.anticon': { 13 | color: token.colorPrimary, 14 | }, 15 | }, 16 | btnName: { 17 | flex: '1 1 0%', 18 | textOverflow: 'ellipsis', 19 | overflow: 'hidden', 20 | whiteSpace: 'nowrap', 21 | }, 22 | linkItem: { 23 | 'padding': '8px 12px', 24 | 'borderRadius': '12px', 25 | 'display': 'flex', 26 | 'alignItems': 'center', 27 | 'gap': '12px', 28 | 'color': token.colorText, 29 | 'fontWeight': 600, 30 | 'fontSize': 16, 31 | '&:hover': { 32 | translate: 'all .15s', 33 | color: token.colorText, 34 | backgroundColor: token.controlItemBgHover, 35 | }, 36 | '.anticon': { 37 | fontSize: 42, 38 | color: token.colorPrimary, 39 | }, 40 | }, 41 | logo: { 42 | height: '50px', 43 | lineHeight: '50px', 44 | borderBottom: `1px solid ${token.colorSplit}`, 45 | color: token.colorPrimary, 46 | padding: '0 20px', 47 | fontSize: '24px', 48 | cursor: 'pointer', 49 | a: { 50 | verticalAlign: 'sub', 51 | }, 52 | svg: { 53 | fill: token.colorTextBase, 54 | }, 55 | }, 56 | chatsTitle: { 57 | padding: '8px 8px 8px 20px', 58 | }, 59 | _title: { 60 | fontSize: 16, 61 | fontWeight: 600, 62 | }, 63 | newbtn: { 64 | 'display': 'flex', 65 | 'alignItems': 'center', 66 | 'cursor': 'pointer', 67 | 'color': token.colorPrimary, 68 | 'padding': '8px 12px', 69 | 'borderRadius': '12px', 70 | '&:hover': { 71 | translate: 'all .15s', 72 | backgroundColor: token.controlItemBgHover, 73 | }, 74 | 'svg': { 75 | marginRight: 8, 76 | width: 20, 77 | }, 78 | }, 79 | })); 80 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/UserInfoBottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import { useTranslations } from 'next-intl'; 6 | import { useRouter } from 'next/navigation'; 7 | import React from 'react'; 8 | 9 | export const useStyles = createStyles(() => { 10 | const defaultHeight = '40px'; 11 | return { 12 | noauthBottomWrapper: { 13 | lineHeight: defaultHeight, 14 | width: '100%', 15 | padding: '8px 12px 0 12px', 16 | overflowY: 'hidden', 17 | }, 18 | loginBtn: { 19 | 'width': '100%', 20 | 'marginBottom': 8, 21 | '.ant-btn': { 22 | width: '100%', 23 | }, 24 | }, 25 | others: { 26 | marginBottom: 8, 27 | }, 28 | icons: { 29 | textAlign: 'right', 30 | lineHeight: defaultHeight, 31 | height: defaultHeight, 32 | }, 33 | aboutus: { 34 | width: 'calc(100% - 80px)', 35 | cursor: 'pointer', 36 | }, 37 | }; 38 | }); 39 | 40 | export default function UserInfoBottom() { 41 | const t = useTranslations(); 42 | const { styles } = useStyles(); 43 | const router = useRouter(); 44 | return ( 45 | 46 | 47 | { 49 | router.push('/oidc/auth'); 50 | }} 51 | size="large" 52 | type="primary" 53 | > 54 | {t('components.index.dengLu')} 55 | 56 | 57 | {/* 61 | 62 | aboutus 63 | 64 | 65 | 66 | 70 | 71 | 72 | */} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import classNames from 'classnames'; 4 | import { usePathname } from 'next/navigation'; 5 | import React from 'react'; 6 | 7 | import { locales } from '@/i18n'; 8 | 9 | import Chats from './Chats'; 10 | import SideBarHeader from './SideBarHeader'; 11 | import UserInfoBottom from './UserInfoBottom'; 12 | import { useStyles } from './styles'; 13 | 14 | export default function SideBar() { 15 | const pathname: any = usePathname(); 16 | const { styles } = useStyles(); 17 | const is_no_sidebar_route = new RegExp( 18 | `^(?:\/(?:${locales.join('|')}))?\/oidc(?:\/.*|\/?|)|\/logout(?:\/.*|\/?)$` 19 | ).test(pathname); 20 | if (is_no_sidebar_route) return <>>; 21 | const showSidebar = ['/chat'].includes(pathname); 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/SideBar/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebar: { 5 | 'position': 'relative', 6 | 'display': 'flex', 7 | 'flexDirection': 'column', 8 | 'flexShrink': '0', 9 | 'width': '336px', 10 | 'height': '100%', 11 | 'backgroundColor': token.colorBgBase, 12 | // 处理 [dir='ltr'] 和 [dir='rtl'] 13 | 'html[dir="ltr"] &': { 14 | borderRight: `1px solid ${token.colorSplit}`, 15 | }, 16 | 'html[dir="rtl"] &': { 17 | borderLeft: `1px solid ${token.colorSplit}`, 18 | }, 19 | 20 | // 媒体查询 21 | '@media (max-width: 879px)': { 22 | '&[sidebar-visible="false"]': { 23 | 'boxShadow': 'none', 24 | 25 | '& button': { 26 | opacity: 0, 27 | }, 28 | }, 29 | 30 | 'html[dir="ltr"] &': { 31 | left: 0, 32 | borderRight: '1px solid', 33 | zIndex: 9, 34 | }, 35 | 36 | 'html[dir="rtl"] &': { 37 | right: 0, 38 | borderLeft: '1px solid', 39 | zIndex: 9, 40 | }, 41 | 42 | '&': { 43 | 'position': 'absolute', 44 | 'width': '100%', 45 | 46 | '&.need_hide_sidebar': { 47 | display: 'none', 48 | }, 49 | }, 50 | 51 | '[dir] &': { 52 | border: 'transparent', 53 | }, 54 | 55 | '[sidebar-visible="false"]': { 56 | '[dir="ltr"] &': { 57 | transform: 'translate(-200%)', 58 | }, 59 | '[dir="rtl"] &': { 60 | transform: 'translate(200%)', 61 | }, 62 | }, 63 | }, 64 | }, 65 | })); 66 | -------------------------------------------------------------------------------- /src/layout/AppNoAuthLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from 'antd'; 2 | 3 | import { User } from '@/types/user'; 4 | 5 | import SideBar from './SideBar'; 6 | 7 | export default function AppLayout({ children }: { children: React.ReactNode; user?: User }) { 8 | return ( 9 | 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/Chats/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Skeleton } from 'antd'; 4 | import classnames from 'classnames'; 5 | import React from 'react'; 6 | 7 | import { useStyles } from './styles'; 8 | 9 | const EMPTY_ITEMS = [1, 2, 3, 4]; 10 | 11 | const Chats: any = () => { 12 | const { styles } = useStyles(); 13 | return ( 14 | 15 | 16 | {EMPTY_ITEMS.map(item => { 17 | return ( 18 | 27 | ); 28 | })} 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default Chats; 35 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/Chats/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(() => ({ 4 | chats: { 5 | position: 'relative', 6 | flex: '1 1 0%', 7 | }, 8 | content: { 9 | 'paddingLeft': 8, 10 | 'paddingBottom': 16, 11 | '.ant-typography': { 12 | marginBottom: 'unset', 13 | }, 14 | }, 15 | emptyItem: { 16 | 'padding': '16px 16px 0 12px', 17 | '.ant-skeleton-header': { 18 | paddingInlineEnd: '8px', 19 | }, 20 | }, 21 | })); 22 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/SideBarHeader/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PlusCircleFilled } from '@ant-design/icons'; 4 | import { Flex } from 'antd'; 5 | import { ChevronRight } from 'lucide-react'; 6 | import { useTranslations } from 'next-intl'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | import React from 'react'; 10 | 11 | import Logo from '@/../public/svg/logo.svg'; 12 | 13 | import { useStyles } from './styles'; 14 | 15 | const SidebarHeader = () => { 16 | const t = useTranslations('SideBarHeader'); 17 | const { styles } = useStyles(); 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {t('index.chuangJianZhiNengTi')} 29 | 30 | 31 | 32 | 33 | {t('index.faXianZhiNengTi')} 34 | 35 | 36 | 37 | 38 | {t('index.duiHua')} 39 | {/* 40 | {t('index.xinDuiHua')} 41 | */} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default SidebarHeader; 48 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/SideBarHeader/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebarHeader: { 5 | overflow: 'hidden', 6 | backgroundColor: token.colorBgBase, 7 | }, 8 | btns: { 9 | 'flex': '1 1 0%', 10 | 'padding': 8, 11 | 'borderBottom': `1px solid ${token.colorSplit}`, 12 | '.anticon': { 13 | color: token.colorPrimary, 14 | }, 15 | }, 16 | btnName: { 17 | flex: '1 1 0%', 18 | textOverflow: 'ellipsis', 19 | overflow: 'hidden', 20 | whiteSpace: 'nowrap', 21 | }, 22 | linkItem: { 23 | 'padding': '8px 12px', 24 | 'borderRadius': '12px', 25 | 'display': 'flex', 26 | 'alignItems': 'center', 27 | 'gap': '12px', 28 | 'color': token.colorText, 29 | 'fontWeight': 600, 30 | 'fontSize': 16, 31 | '&:hover': { 32 | translate: 'all .15s', 33 | color: token.colorText, 34 | backgroundColor: token.controlItemBgHover, 35 | }, 36 | '.anticon': { 37 | fontSize: 42, 38 | color: token.colorPrimary, 39 | }, 40 | }, 41 | logo: { 42 | height: '50px', 43 | lineHeight: '50px', 44 | borderBottom: `1px solid ${token.colorSplit}`, 45 | color: token.colorPrimary, 46 | padding: '0 20px', 47 | fontSize: '24px', 48 | cursor: 'pointer', 49 | a: { 50 | verticalAlign: 'sub', 51 | }, 52 | svg: { 53 | fill: token.colorTextBase, 54 | }, 55 | }, 56 | chatsTitle: { 57 | padding: '8px 8px 8px 20px', 58 | }, 59 | _title: { 60 | fontSize: 16, 61 | fontWeight: 600, 62 | }, 63 | newbtn: { 64 | 'display': 'flex', 65 | 'alignItems': 'center', 66 | 'cursor': 'pointer', 67 | 'color': token.colorPrimary, 68 | 'padding': '8px 12px', 69 | 'borderRadius': '12px', 70 | '&:hover': { 71 | translate: 'all .15s', 72 | backgroundColor: token.controlItemBgHover, 73 | }, 74 | 'svg': { 75 | marginRight: 8, 76 | width: 20, 77 | }, 78 | }, 79 | })); 80 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/UserInfoBottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Skeleton } from 'antd'; 4 | import { createStyles } from 'antd-style'; 5 | import React from 'react'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | export const useStyles = createStyles(({ token }) => { 9 | const defaultHeight = '60px'; 10 | return { 11 | userinfo: { 12 | lineHeight: defaultHeight, 13 | height: defaultHeight, 14 | width: '100%', 15 | padding: '0 12px', 16 | overflowY: 'hidden', 17 | borderTop: `1px solid ${token.colorSplit}`, 18 | alignItems: 'center', 19 | display: 'flex', 20 | }, 21 | skeletonUserinfo: { 22 | 'height': defaultHeight, 23 | 'display': 'flex', 24 | 'alignItems': 'center', 25 | 'paddingTop': '15px', 26 | '.ant-skeleton': { 27 | 'height': '40px', 28 | 'paddingLeft': '12px', 29 | 'display': 'block', 30 | '.ant-skeleton-header': { 31 | paddingInlineEnd: '12px', 32 | }, 33 | '.ant-skeleton-paragraph': { 34 | marginBottom: 0, 35 | paddingTop: '7px', 36 | }, 37 | }, 38 | }, 39 | checkIcon: { 40 | marginRight: 8, 41 | verticalAlign: 'bottom', 42 | width: '15px', 43 | }, 44 | hideIcon: { 45 | marginRight: 8, 46 | verticalAlign: 'bottom', 47 | width: '15px', 48 | color: 'rgba(0, 0, 0, 0)', 49 | }, 50 | icons: { 51 | textAlign: 'right', 52 | lineHeight: defaultHeight, 53 | paddingTop: '10px', 54 | }, 55 | avator: { 56 | minWidth: '38px', 57 | }, 58 | username: { 59 | width: 'calc(100% - 80px)', 60 | cursor: 'pointer', 61 | }, 62 | hover: { 63 | 'height': '40px', 64 | 'lineHeight': '40px', 65 | 'padding': '0 12px', 66 | '.ant-typography': { 67 | lineHeight: '40px', 68 | maxWidth: 'calc(100% - 40px)', 69 | overflow: 'hidden', 70 | textOverflow: 'ellipsis', 71 | }, 72 | '&:hover': { 73 | color: 'rgba(0, 0, 0, 0.88)', 74 | backgroundColor: token.controlItemBgHover, 75 | borderRadius: '12px', 76 | }, 77 | }, 78 | }; 79 | }); 80 | 81 | export default function UserInfoBottom() { 82 | const { styles } = useStyles(); 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import classNames from 'classnames'; 4 | import { usePathname } from 'next/navigation'; 5 | import React from 'react'; 6 | 7 | import { locales } from '@/i18n'; 8 | 9 | import Chats from './Chats'; 10 | import SideBarHeader from './SideBarHeader'; 11 | import UserInfoBottom from './UserInfoBottom'; 12 | import { useStyles } from './styles'; 13 | 14 | export default function SideBar() { 15 | const pathname: any = usePathname(); 16 | const { styles } = useStyles(); 17 | const is_no_sidebar_route = new RegExp( 18 | `^(?:\/(?:${locales.join('|')}))?\/oidc(?:\/.*|\/?|)|\/logout(?:\/.*|\/?)$` 19 | ).test(pathname); 20 | if (is_no_sidebar_route) return <>>; 21 | const showSidebar = ['/chat'].includes(pathname); 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/SideBar/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => ({ 4 | sidebar: { 5 | 'position': 'relative', 6 | 'display': 'flex', 7 | 'flexDirection': 'column', 8 | 'flexShrink': '0', 9 | 'width': '336px', 10 | 'height': '100%', 11 | 'backgroundColor': token.colorBgBase, 12 | // 处理 [dir='ltr'] 和 [dir='rtl'] 13 | 'html[dir="ltr"] &': { 14 | borderRight: `1px solid ${token.colorSplit}`, 15 | }, 16 | 'html[dir="rtl"] &': { 17 | borderLeft: `1px solid ${token.colorSplit}`, 18 | }, 19 | 20 | // 媒体查询 21 | '@media (max-width: 879px)': { 22 | '&[sidebar-visible="false"]': { 23 | 'boxShadow': 'none', 24 | 25 | '& button': { 26 | opacity: 0, 27 | }, 28 | }, 29 | 30 | 'html[dir="ltr"] &': { 31 | left: 0, 32 | borderRight: '1px solid', 33 | zIndex: 9, 34 | }, 35 | 36 | 'html[dir="rtl"] &': { 37 | right: 0, 38 | borderLeft: '1px solid', 39 | zIndex: 9, 40 | }, 41 | 42 | '&': { 43 | 'position': 'absolute', 44 | 'width': '100%', 45 | 46 | '&.need_hide_sidebar': { 47 | display: 'none', 48 | }, 49 | }, 50 | 51 | '[dir] &': { 52 | border: 'transparent', 53 | }, 54 | 55 | '[sidebar-visible="false"]': { 56 | '[dir="ltr"] &': { 57 | transform: 'translate(-200%)', 58 | }, 59 | '[dir="rtl"] &': { 60 | transform: 'translate(200%)', 61 | }, 62 | }, 63 | }, 64 | }, 65 | })); 66 | -------------------------------------------------------------------------------- /src/layout/AppSkeletonLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from 'antd'; 2 | 3 | import { User } from '@/types/user'; 4 | 5 | import SideBar from './SideBar'; 6 | 7 | export default function AppLayout({ children }: { children: React.ReactNode; user?: User }) { 8 | return ( 9 | 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/layout/AuthLayout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { 4 | Dispatch, 5 | PropsWithChildren, 6 | SetStateAction, 7 | createContext, 8 | useContext, 9 | useState, 10 | } from 'react'; 11 | 12 | interface AuthContextType { 13 | authed: boolean | undefined; 14 | setAuthed: Dispatch>; 15 | } 16 | 17 | // 创建一个 Context 18 | const AuthContext = createContext({ 19 | authed: undefined, // 验证是否存在/是否过期 => 可用: true, 不可用: false, 未验证 undefined 20 | setAuthed: () => {}, 21 | }); 22 | 23 | // Context 提供者组件 24 | export const AuthLayout = ({ children }: PropsWithChildren) => { 25 | const [authed, setAuthed] = useState(); 26 | 27 | return {children}; 28 | }; 29 | 30 | // Hook 用于访问 Context 31 | export const useAuthContext = () => useContext(AuthContext); 32 | 33 | export default AuthLayout; 34 | -------------------------------------------------------------------------------- /src/layout/AxiosConfigLayout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { 4 | Dispatch, 5 | PropsWithChildren, 6 | SetStateAction, 7 | createContext, 8 | useContext, 9 | useState, 10 | } from 'react'; 11 | 12 | interface AxiosConfigContextType { 13 | isAxiosConfigured: boolean; 14 | setAxiosConfigured: Dispatch>; 15 | } 16 | 17 | // 创建一个 Context 18 | const AxiosConfigContext = createContext({ 19 | isAxiosConfigured: false, 20 | setAxiosConfigured: () => {}, 21 | }); 22 | 23 | // Context 提供者组件 24 | export const AxiosConfigLayout = ({ children }: PropsWithChildren) => { 25 | const [isAxiosConfigured, setAxiosConfigured] = useState(false); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | // Hook 用于访问 Context 35 | export const useAxiosConfig = () => useContext(AxiosConfigContext); 36 | 37 | export default AxiosConfigLayout; 38 | -------------------------------------------------------------------------------- /src/layout/GlobalLayout/ThemeLayout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { App, ConfigProvider } from 'antd'; 4 | import { ThemeMode, ThemeProvider } from 'antd-style'; 5 | import 'antd/dist/reset.css'; 6 | import { Locale } from 'antd/lib/locale'; 7 | import cloneDeep from 'lodash/cloneDeep'; 8 | import merge from 'lodash/merge'; 9 | import { usePathname } from 'next/navigation'; 10 | import React, { PropsWithChildren } from 'react'; 11 | import { useSelector } from 'react-redux'; 12 | 13 | import { useAuthContext } from '@/layout/AuthLayout'; 14 | import { GlobalStyle } from '@/styles'; 15 | import { dark, default_theme, light } from '@/theme/themeConfig'; 16 | import { setAxiosHooksWithAuth } from '@/utils/axios'; 17 | import { isTokenExpired, setCookie } from '@/utils/client'; 18 | import { AUTH_DATA } from '@/utils/constants'; 19 | 20 | import { useAxiosConfig } from '../../AxiosConfigLayout'; 21 | 22 | interface Props extends PropsWithChildren { 23 | theme?: ThemeMode; // 刷新页面时, 从 cookie 获取保存的 theme, 作为初始值 24 | client_theme?: 'dark' | 'light' | undefined; // client theme 25 | antdLocale: Locale; 26 | locale: string; 27 | } 28 | 29 | const ThemeLayout = React.memo( 30 | ({ children, theme: init_page_theme, client_theme, antdLocale, locale }) => { 31 | const { setAxiosConfigured, isAxiosConfigured } = useAxiosConfig(); 32 | const { setAuthed } = useAuthContext(); 33 | const [theme, setTheme] = React.useState( 34 | init_page_theme === 'auto' ? client_theme || 'auto' : init_page_theme 35 | ); 36 | const [mediaQuery, setMediaQuery] = React.useState(); 37 | const theme_from_store = useSelector((store: any) => store.theme); 38 | const pathname = usePathname(); 39 | 40 | const NO_AUTH_ROUTES = React.useMemo( 41 | () => 42 | new Set([ 43 | `/${locale}/oidc/callback`, 44 | `/${locale}/oidc/logout`, 45 | `/${locale}/oidc/remove-auth-and-login`, 46 | `/${locale}/oidc/auth`, 47 | ]), 48 | [locale] 49 | ); 50 | React.useEffect(() => { 51 | if (NO_AUTH_ROUTES.has(pathname)) { 52 | return; 53 | } 54 | const auth = localStorage.getItem(AUTH_DATA); 55 | let authObj: any; 56 | try { 57 | authObj = JSON.parse(auth || '{}'); 58 | } catch { 59 | // console.warn('no auth or parse auth err', _); 60 | } 61 | if (!auth || isTokenExpired(authObj?.token.id_token)) { 62 | // router.push('/oidc/auth'); // 暂时屏蔽 => 自动跳转认证 63 | setAuthed(false); 64 | return; 65 | } 66 | setAuthed(true); 67 | if (!isAxiosConfigured) { 68 | setAxiosConfigured(setAxiosHooksWithAuth()); 69 | } 70 | }, [pathname]); 71 | 72 | const handleThemeChange = React.useCallback( 73 | (e: MediaQueryListEvent) => { 74 | if (theme_from_store !== 'auto') return; 75 | if (e.matches) { 76 | // 系统为: 暗黑模式 77 | setCookie('client_theme', 'dark'); 78 | setTheme('dark'); 79 | } else { 80 | // 系统为: 正常(亮色)模式 81 | setCookie('client_theme', 'light'); 82 | setTheme('light'); 83 | } 84 | }, 85 | [theme_from_store, setTheme] 86 | ); 87 | 88 | React.useEffect(() => { 89 | setMediaQuery(window.matchMedia('(prefers-color-scheme: dark)')); 90 | }, []); 91 | React.useEffect(() => { 92 | if (mediaQuery) { 93 | mediaQuery.addListener(handleThemeChange); 94 | return () => { 95 | mediaQuery.removeListener(handleThemeChange); 96 | }; 97 | } 98 | }, [mediaQuery, handleThemeChange]); 99 | React.useEffect(() => { 100 | if (theme_from_store !== 'auto' && theme_from_store !== theme) { 101 | setTheme(theme_from_store); 102 | } 103 | if (theme_from_store === 'auto' && mediaQuery) { 104 | handleThemeChange(mediaQuery); 105 | return; 106 | } 107 | }, [theme_from_store, mediaQuery]); 108 | const themeConfig = React.useMemo( 109 | () => 110 | theme === 'auto' 111 | ? default_theme 112 | : merge(cloneDeep(default_theme), theme === 'dark' ? dark : light), 113 | [theme] 114 | ); 115 | return ( 116 | 122 | 123 | 124 | 125 | {children} 126 | 127 | 128 | 129 | ); 130 | } 131 | ); 132 | 133 | export default ThemeLayout; 134 | -------------------------------------------------------------------------------- /src/layout/GlobalLayout/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --background-start-rgb: 214, 219, 220; 5 | --background-end-rgb: 255, 255, 255; 6 | --primary-glow: conic-gradient( 7 | from 180deg at 50% 50%, 8 | #16abff33 0deg, 9 | #0885ff33 55deg, 10 | #54d6ff33 120deg, 11 | #0071ff33 160deg, 12 | transparent 360deg 13 | ); 14 | --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); 15 | --callout-rgb: 238, 240, 241; 16 | --callout-border-rgb: 172, 175, 176; 17 | --card-rgb: 180, 185, 188; 18 | --card-border-rgb: 131, 134, 135; 19 | } 20 | 21 | * { 22 | user-select: none; 23 | box-sizing: border-box; 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | body, 29 | html { 30 | overflow-x: hidden; 31 | width: 100vw; 32 | height: 100vh; 33 | } 34 | 35 | a { 36 | text-decoration: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/layout/GlobalLayout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeMode } from 'antd-style'; 4 | import enUS from 'antd/locale/en_US'; 5 | import zhCN from 'antd/locale/zh_CN'; 6 | import React, { PropsWithChildren } from 'react'; 7 | import { Provider } from 'react-redux'; 8 | 9 | import { useStore } from '@/store'; 10 | 11 | import ThemeLayout from './ThemeLayout'; 12 | 13 | interface GlobalLayoutProps extends PropsWithChildren { 14 | theme: ThemeMode | undefined; // theme from cookie; 15 | client_theme: 'dark' | 'light' | undefined; 16 | locale: string; 17 | } 18 | 19 | const GlobalLayout = React.memo(({ children, theme, locale, client_theme }) => { 20 | const store = useStore({ 21 | theme, 22 | }); 23 | return ( 24 | 25 | 31 | {children} 32 | 33 | 34 | ); 35 | }); 36 | 37 | export default GlobalLayout; 38 | -------------------------------------------------------------------------------- /src/layout/PWAHandlerLayout/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createContext, useContext, useEffect, useState } from 'react'; 4 | 5 | // 创建一个上下文来共享安装提示事件 6 | const InstallPromptContext = createContext(null); 7 | 8 | export function useInstallPrompt() { 9 | return useContext(InstallPromptContext); 10 | } 11 | 12 | export default function PWAHandlerLayout({ children }: { children: React.ReactNode }) { 13 | const [installPrompt, setInstallPrompt] = useState(null); 14 | 15 | useEffect(() => { 16 | const handleBeforeInstallPrompt = (e: Event) => { 17 | e.preventDefault(); 18 | console.warn('beforeinstallprompt event fired ==========='); 19 | setInstallPrompt(e); 20 | }; 21 | 22 | window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); 23 | 24 | return () => { 25 | window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); 26 | }; 27 | }, []); 28 | 29 | return ( 30 | {children} 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/layout/StyleRegistry.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { StyleProvider, extractStaticStyle } from 'antd-style'; 4 | import { useServerInsertedHTML } from 'next/navigation'; 5 | import { PropsWithChildren, useRef } from 'react'; 6 | 7 | const StyleRegistry = ({ children }: PropsWithChildren) => { 8 | const isInsert = useRef(false); 9 | 10 | useServerInsertedHTML(() => { 11 | // 避免多次渲染时重复插入样式 12 | // refs: https://github.com/vercel/next.js/discussions/49354#discussioncomment-6279917 13 | if (isInsert.current) return; 14 | 15 | isInsert.current = true; 16 | 17 | const styles = extractStaticStyle().map(item => item.style); 18 | 19 | return <>{styles}>; 20 | }); 21 | 22 | return {children}; 23 | }; 24 | 25 | export default StyleRegistry; 26 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from 'next-intl/middleware'; 2 | import { NextRequest } from 'next/server'; 3 | 4 | import { locales } from './i18n'; 5 | import { LOCALE } from './utils/constants'; 6 | 7 | export default async function middleware(request: NextRequest) { 8 | const acceptLanguage = 9 | request.headers.get('accept-language')?.split(';')?.[0]?.split(',')?.[0]?.split('-')?.[0] || ''; 10 | const defaultLocale: string = 11 | request.cookies.get(LOCALE)?.value || locales.includes(acceptLanguage) ? acceptLanguage : 'en'; 12 | 13 | const handleI18nRouting = createMiddleware({ 14 | locales, 15 | defaultLocale, 16 | }); 17 | const response = handleI18nRouting(request); 18 | 19 | response.headers.set(LOCALE, defaultLocale); 20 | 21 | return response; 22 | } 23 | 24 | export const config = { 25 | // Match only internationalized pathnames 26 | matcher: [ 27 | // Enable a redirect to a matching locale at the root 28 | // '/', 29 | 30 | // Set a cookie to remember the previous locale for 31 | // all requests that have a locale prefix 32 | // `/(${locales.join('|')})/:path*`, 33 | 34 | // Enable redirects that add missing locales 35 | // (e.g. `/pathnames` -> `/en/pathnames`) 36 | '/((?!_next|_vercel|.*\\..*).*)', 37 | 38 | // '!(manifest.json)', 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { configureStore } from '@reduxjs/toolkit'; 4 | import { useMemo } from 'react'; 5 | 6 | import { setCookie } from '@/utils/client'; 7 | import { AUTH_DATA } from '@/utils/constants'; 8 | 9 | let store: any; 10 | 11 | const reducer = (state = {}, action: any) => { 12 | switch (action.type) { 13 | case 'TRIGGER_THEME': { 14 | setCookie('theme', action.theme); // todo remove, use user profile by bff ? 15 | return { 16 | ...state, 17 | theme: action.theme, 18 | }; 19 | } 20 | case 'TRIGGER_PAGE_LOADING': { 21 | return { 22 | ...state, 23 | pageLoading: action.pageLoading, 24 | }; 25 | } 26 | case 'SAVE_AUTH_DATA': { 27 | return { 28 | ...state, 29 | [AUTH_DATA]: action[AUTH_DATA], 30 | }; 31 | } 32 | default: { 33 | return state; 34 | } 35 | } 36 | }; 37 | 38 | function initStore(preloadedState = {}) { 39 | return configureStore({ 40 | reducer, 41 | preloadedState, 42 | devTools: process.env.NODE_ENV !== 'production', 43 | }); 44 | } 45 | 46 | export const initializeStore = (preloadedState: any) => { 47 | let _store = store ?? initStore(preloadedState); 48 | 49 | // After navigating to a page with an initial Redux state, merge that state 50 | // with the current state in the store, and create a new store 51 | if (preloadedState && store) { 52 | _store = initStore({ 53 | ...store.getState(), 54 | ...preloadedState, 55 | }); 56 | // Reset the current store 57 | store = undefined; 58 | } 59 | 60 | // For SSG and SSR always create a new store 61 | if (typeof window === 'undefined') return _store; 62 | // Create the store once in the client 63 | if (!store) store = _store; 64 | 65 | return _store; 66 | }; 67 | 68 | export function useStore(_initialState?: any) { 69 | const init = _initialState || {}; 70 | const _store = useMemo(() => initializeStore(init), [init]); 71 | return _store; 72 | } 73 | -------------------------------------------------------------------------------- /src/styles/antdOverride.ts: -------------------------------------------------------------------------------- 1 | import { Theme, css } from 'antd-style'; 2 | 3 | export default ({ token }: { prefixCls: string; token: Theme }) => css` 4 | .${token.prefixCls}-popover { 5 | z-index: 1100; 6 | } 7 | .${token.prefixCls}-upload-wrapper.${token.prefixCls}-upload-picture-circle-wrapper 8 | .${token.prefixCls}-upload.${token.prefixCls}-upload-select { 9 | margin-inline-end: 0; 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'antd-style'; 2 | 3 | export default ({ prefixCls, token }: { prefixCls: string; token: any }) => css` 4 | html, 5 | body, 6 | #__next, 7 | .${prefixCls}-app { 8 | position: relative; 9 | 10 | overflow: hidden; 11 | overscroll-behavior: none; 12 | 13 | height: 100% !important; 14 | min-height: 100% !important; 15 | } 16 | 17 | body { 18 | color: CanvasText; 19 | color-scheme: light dark; 20 | background-color: Canvas !important; 21 | } 22 | 23 | ::-webkit-scrollbar { 24 | display: none; 25 | width: 0; 26 | height: 0; 27 | } 28 | .scrollBar { 29 | position: relative; 30 | z-index: 1; 31 | 32 | overflow-y: scroll; 33 | 34 | width: 100%; 35 | max-height: 100%; 36 | } 37 | .scrollBar:not(:hover) { 38 | padding-right: 7px; 39 | } 40 | .scrollBar:hover::-webkit-scrollbar { 41 | display: initial; 42 | width: 7px; 43 | height: 0; 44 | } 45 | 46 | .scrollBar:hover::-webkit-scrollbar-track { 47 | background: transparent; 48 | } 49 | 50 | .scrollBar:hover::-webkit-scrollbar-thumb { 51 | background-color: #ccc; 52 | background-clip: padding-box; 53 | border-bottom: 1px solid transparent; 54 | } 55 | 56 | [dir='ltr'] .scrollBar:hover::-webkit-scrollbar-thumb { 57 | border-right: 1px solid transparent; 58 | border-radius: 6px 8px 8px; 59 | } 60 | 61 | [dir='rtl'] .scrollBar:hover::-webkit-scrollbar-thumb { 62 | border-left: 1px solid transparent; 63 | border-radius: 8px 6px 8px 8px; 64 | } 65 | 66 | .scrollBar:hover::-webkit-scrollbar-thumb:hover { 67 | background-color: #999; 68 | background-clip: padding-box; 69 | } 70 | 71 | [dir='ltr'] .scrollBar:hover::-webkit-scrollbar-thumb:hover { 72 | border-right: 1px solid transparent; 73 | } 74 | 75 | [dir='rtl'] .scrollBar:hover::-webkit-scrollbar-thumb:hover { 76 | border-left: 1px solid transparent; 77 | } 78 | 79 | p { 80 | margin-bottom: 0; 81 | } 82 | 83 | @media (max-width: 575px) { 84 | * { 85 | ::-webkit-scrollbar { 86 | display: none; 87 | width: 0; 88 | height: 0; 89 | } 90 | } 91 | } 92 | 93 | @keyframes inactivelink-hover-animation { 94 | 0% { 95 | background-color: transparent; 96 | } 97 | to { 98 | background-color: ${token.controlItemBgHover}; 99 | } 100 | } 101 | `; 102 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'antd-style'; 2 | 3 | import antdOverride from './antdOverride'; 4 | import global from './global'; 5 | 6 | const prefixCls = 'ant'; 7 | 8 | export const GlobalStyle = createGlobalStyle(({ theme }) => [ 9 | global({ prefixCls, token: theme }), 10 | antdOverride({ prefixCls, token: theme }), 11 | ]); 12 | -------------------------------------------------------------------------------- /src/styles/not-found-styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from 'antd-style'; 2 | 3 | export const useStyles = createStyles(({ token }) => { 4 | return { 5 | wrapper404: { 6 | height: '100vh', 7 | width: '100%', 8 | position: 'absolute', 9 | top: 0, 10 | left: 0, 11 | zIndex: 10, 12 | backgroundColor: token.colorBgBase, 13 | }, 14 | 15 | content: { 16 | position: 'absolute', 17 | top: '50%', 18 | left: '50%', 19 | transform: 'translate(-50%, -50%)', 20 | textAlign: 'center', 21 | }, 22 | 23 | imgBg: { 24 | height: '420px', 25 | width: '500px', 26 | maxWidth: '100vw', 27 | marginBottom: '24px', 28 | paddingLeft: '35px', 29 | }, 30 | 31 | text: { 32 | fontSize: '24px', 33 | marginBottom: '24px', 34 | }, 35 | btn: { 36 | '.ant-btn': { 37 | paddingLeft: '30px', 38 | paddingRight: '30px', 39 | fontSize: '16px', 40 | height: 'auto', 41 | }, 42 | }, 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /src/theme/themeConfig.ts: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd'; 2 | import { ThemeProviderProps as AntDThemeProviderProps } from 'antd-style'; 3 | 4 | export type ThemeProviderProps = AntDThemeProviderProps; 5 | 6 | const colorPrimary = '#A060EE'; 7 | 8 | const default_theme_props: ThemeProviderProps = Object.freeze({ 9 | customToken: { 10 | testHeight: '50px', 11 | colorPrimaryTest: '#f85a5a', 12 | }, 13 | theme: { 14 | algorithm: theme.defaultAlgorithm, 15 | token: { 16 | colorPrimary, 17 | borderRadius: 12, 18 | colorLink: colorPrimary, 19 | }, 20 | }, 21 | }); 22 | 23 | const light_theme_props: ThemeProviderProps = Object.freeze({ 24 | theme: { 25 | token: { 26 | colorBgBase: '#fff', 27 | colorBgLayout: '#f5f5f5', 28 | colorSplit: 'rgba(0, 0, 0, 0.06)', 29 | }, 30 | }, 31 | }); 32 | 33 | const dark_theme_props: ThemeProviderProps = Object.freeze({ 34 | theme: { 35 | algorithm: theme.darkAlgorithm, 36 | token: { 37 | colorBgBase: '#000', 38 | colorBgLayout: '#141414', 39 | colorTextBase: '#fff', 40 | colorSplit: 'rgba(255, 255, 255, .16)', 41 | colorText: 'rgba(255, 255, 255, .85)', 42 | colorTextSecondary: 'rgba(255, 255, 255, .65)', 43 | colorTextTertiary: 'rgba(255, 255, 255, .45)', 44 | colorTextQuaternary: 'rgba(255, 255, 255, .25)', 45 | }, 46 | }, 47 | }); 48 | 49 | export const light = light_theme_props; 50 | export const dark = dark_theme_props; 51 | export const default_theme = default_theme_props; 52 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | name: string; 3 | full_name?: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Axios, { AxiosInstance, AxiosRequestConfig, CreateAxiosDefaults } from 'axios'; 4 | import useAxios, { Options, UseAxiosResult, configure } from 'axios-hooks'; 5 | import merge from 'lodash/merge'; 6 | import { useEffect } from 'react'; 7 | 8 | import { useAxiosConfig } from '@/layout/AxiosConfigLayout'; 9 | 10 | import { AUTH_DATA } from './constants'; 11 | 12 | export const getAuthData = () => { 13 | try { 14 | if (typeof window === 'undefined') throw new Error('should in client side'); 15 | return JSON.parse(localStorage.getItem(AUTH_DATA) || '{}'); 16 | } catch (error) { 17 | console.warn('getAuthData failed', error); 18 | return {}; 19 | } 20 | }; 21 | 22 | export const createCustomAxios = (configs?: CreateAxiosDefaults): AxiosInstance => { 23 | const { token } = getAuthData(); 24 | if (!token) { 25 | console.warn('create axios instance error'); 26 | return {}; 27 | } 28 | const { token_type, id_token } = token; 29 | const authHeaders = { 30 | authorization: `${token_type} ${id_token}`, 31 | }; 32 | const _configs = merge( 33 | { 34 | headers: authHeaders, 35 | }, 36 | configs || {} 37 | ); 38 | return Axios.create(_configs); 39 | }; 40 | 41 | export const setAxiosHooksWithAuth = () => { 42 | const axios = createCustomAxios(); 43 | configure({ axios }); 44 | return true; // 初始化成功 返回标记 45 | }; 46 | 47 | export const setAxiosHooksWithoutAuth = () => { 48 | configure({ axios: Axios.create({}) }); 49 | }; 50 | 51 | export const useAxiosRequest = ( 52 | config: AxiosRequestConfig, 53 | options?: Options, 54 | executeConfig?: AxiosRequestConfig, 55 | executeOptions?: Options 56 | ): UseAxiosResult => { 57 | const { isAxiosConfigured } = useAxiosConfig(); 58 | const _options = Object.assign( 59 | { 60 | manual: true, // 手动执行 61 | }, 62 | options 63 | ); 64 | const [{ data, loading, error }, execute, manualCancel] = useAxios(config, _options); 65 | 66 | useEffect(() => { 67 | if (isAxiosConfigured) { 68 | execute(executeConfig, executeOptions); // 进入页面且 isAxiosConfigured 执行接口调用 69 | } 70 | }, [isAxiosConfigured, execute]); 71 | 72 | return [{ data, loading, error }, execute, manualCancel]; 73 | }; 74 | -------------------------------------------------------------------------------- /src/utils/client.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LOGIN_REDIRECT } from './constants'; 4 | 5 | export { sdk as bff } from '@yuntijs/arcadia-bff-sdk'; 6 | export { sdk as bffClient } from '@yuntijs/bff-client'; 7 | 8 | export const getCookie = (cookieString: string, cookieName: string) => { 9 | const name = `${cookieName}=`; 10 | const decodedCookie = decodeURIComponent(cookieString); 11 | const ca = decodedCookie.split(';'); 12 | 13 | for (const cookie of ca) { 14 | const c = cookie.trim(); 15 | if (c.startsWith(name)) { 16 | return c.slice(name.length); 17 | } 18 | } 19 | return ''; 20 | }; 21 | 22 | /** 23 | * 设置一个 cookie。 24 | * @param {string} name Cookie 的名称。 25 | * @param {string} value Cookie 的值。 26 | * @param {number} [days] Cookie 的过期时间(天数)。如果不设置,默认为会话 Cookie。 27 | * @param {string} [path] Cookie 的路径。默认为根路径 '/'。 28 | */ 29 | export const setCookie = (name: string, value: string, days?: number, path?: string) => { 30 | let expires = ''; 31 | 32 | if (days) { 33 | const date = new Date(); 34 | date.setTime(date.getTime() + (days || 1) * 24 * 60 * 60 * 1000); 35 | expires = `; expires=${date.toUTCString()}`; 36 | } 37 | if (typeof window.document === 'object') { 38 | // eslint-disable-next-line unicorn/no-document-cookie 39 | window.document.cookie = `${name}=${value}${expires}; path=${path || '/'}`; 40 | } 41 | }; 42 | 43 | interface ParsedToken { 44 | alg: string; 45 | kid: string; 46 | iss: string; 47 | sub: string; 48 | aud: string; 49 | exp: number; 50 | iat: number; 51 | at_hash: string; 52 | c_hash: string; 53 | email: string; 54 | email_verified: boolean; 55 | groups: string[]; 56 | name: string; 57 | preferred_username: string; 58 | } 59 | 60 | /** 61 | * 判断 auth 是否过期。 62 | * @param {string} id_token token.id_token without last part。 63 | */ 64 | 65 | function parseToken(token: string[]): ParsedToken { 66 | return token 67 | .map(str => { 68 | try { 69 | return JSON.parse(atob(str)); 70 | } catch (error) { 71 | console.warn('parer token err', error); 72 | } 73 | return {}; 74 | }) 75 | .reduce( 76 | (pr, cu) => ({ 77 | ...pr, 78 | ...cu, 79 | }), 80 | {} 81 | ); 82 | } 83 | 84 | export function isTokenExpired(id_token?: string): boolean { 85 | if (!id_token) { 86 | return true; 87 | } 88 | const id_token_split_arr = (() => { 89 | const arr = id_token.split('.'); 90 | arr.pop(); 91 | return arr; 92 | })(); 93 | const expiredTimestampInMs = parseToken(id_token_split_arr).exp * 1000; 94 | return Date.now() >= expiredTimestampInMs; 95 | } 96 | 97 | /** 98 | * 设置登录 redirect 99 | * @param {string} redirectUrl: e.g. '/chat'. 100 | */ 101 | export function setLoginRedirect(redirectUrl: string) { 102 | setCookie(LOGIN_REDIRECT, redirectUrl); 103 | } 104 | 105 | /** 106 | * 获取登录 redirect 107 | */ 108 | export function getLoginRedirect(cookieString: string) { 109 | return getCookie(cookieString, LOGIN_REDIRECT); 110 | } 111 | 112 | /** 113 | * 移除登录 redirect 114 | */ 115 | export function delLoginRedirect() { 116 | setCookie(LOGIN_REDIRECT, '', -1); 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_DATA = 'authData'; 2 | export const DEFAULT_CHAT = 'default_chat'; 3 | export const LOCALE = 'NEXT_LOCALE'; 4 | export const LOGIN_REDIRECT = 'LOGIN_REDIRECT'; 5 | export const AGENT_CATEGORY_INDEXES = [ 6 | 'components.index.tuiJian', 7 | 'utils.constants.youXiDongMan', 8 | 'create.page.tongYongDuiHua', 9 | 'create.page.gongZuoXueXi', 10 | 'create.page.neiRongChuangZuo', 11 | 'utils.constants.aIHuiHua', 12 | 'create.page.yingYinShengCheng', 13 | 'create.page.jueSeBanYan', 14 | 'create.page.shengHuoQuWei', 15 | 'create.page.qiTa', 16 | ]; 17 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import { UAParser } from 'ua-parser-js'; 3 | 4 | // todo: server side bff err 5 | // import { sdk } from '@yuntijs/arcadia-bff-sdk'; 6 | // export const bff = sdk; 7 | 8 | /** 9 | * check mobile device in server 10 | */ 11 | export const isMobileDevice = () => { 12 | if (typeof process === 'undefined') { 13 | throw new TypeError('[Server method] you are importing a server-only module outside of server'); 14 | } 15 | 16 | const { get } = headers(); 17 | const ua = get('user-agent'); 18 | 19 | // console.debug(ua); 20 | const device = new UAParser(ua || '').getDevice(); 21 | return device.type === 'mobile'; 22 | }; 23 | 24 | export const atob = (encodedData: string) => { 25 | return Buffer.from(encodedData, 'base64').toString(); 26 | }; 27 | 28 | export const btoa = (stringToEncode: string) => { 29 | return Buffer.from(stringToEncode).toString('base64'); 30 | }; 31 | 32 | export const getOriginServerSide = () => { 33 | const heads = headers(); 34 | if (!heads.get('x-forwarded-proto') || !heads.get('host')) { 35 | throw new Error('get origin err'); 36 | } 37 | return `${heads.get('x-forwarded-proto')}://${heads.get('host')}`; 38 | }; 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "importHelpers": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "plugins": [ 24 | { 25 | "name": "next" 26 | } 27 | ] 28 | }, 29 | "exclude": ["node_modules"], 30 | "include": [ 31 | "next-env.d.ts", 32 | "src", 33 | "tests", 34 | "**/*.ts", 35 | "**/*.d.ts", 36 | "**/*.tsx", 37 | ".next/types/**/*.ts" 38 | ], 39 | "ts-node": { 40 | "compilerOptions": { 41 | "module": "commonjs" 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------
105 | Get started by editing 106 | src/app/page.tsx 107 |
src/app/page.tsx