├── .env.exmaple ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── docs ├── banner.png └── qrcode.png ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── favicon.svg ├── src ├── api │ ├── app.ts │ ├── billing.ts │ ├── chat.ts │ ├── salesman.ts │ ├── task.ts │ └── user.ts ├── assets │ └── poster.png ├── components │ ├── Icon.tsx │ ├── Markdown.tsx │ ├── SvgIcon.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── constants.ts ├── hooks │ ├── use-app-config.ts │ ├── use-auth.ts │ ├── use-mobile-code.ts │ ├── use-mobile-screen.ts │ ├── use-share-openid.ts │ ├── use-task.tsx │ └── use-wechat.ts ├── layout │ ├── Header.tsx │ ├── TabBar.tsx │ ├── TitleHeader.tsx │ └── index.tsx ├── locales │ ├── en.json │ ├── index.ts │ └── zh.json ├── main.tsx ├── pages │ ├── billing │ │ ├── BillingRecords.tsx │ │ ├── OfflinePayDialog.tsx │ │ ├── PayDialog.tsx │ │ └── index.tsx │ ├── chat │ │ ├── Chat.tsx │ │ ├── ChatItem.tsx │ │ ├── Conversation.tsx │ │ ├── MessageExporter.tsx │ │ ├── RoleListDialog.tsx │ │ └── index.tsx │ ├── error-page.tsx │ ├── login │ │ ├── LoginForm.tsx │ │ ├── Protocol.tsx │ │ ├── QrCodeDialog.tsx │ │ └── index.tsx │ ├── salesman │ │ ├── ListScroll.tsx │ │ ├── WithdrawalDialog.tsx │ │ ├── WithdrawalListDialog.tsx │ │ └── index.tsx │ └── user │ │ ├── ShareDialog.tsx │ │ └── index.tsx ├── router │ └── index.tsx ├── store │ ├── app.ts │ ├── billing.ts │ ├── chat.ts │ ├── index.ts │ ├── salesman.ts │ └── user.ts ├── styles │ └── tailwind.less └── utils │ ├── index.ts │ ├── request.ts │ └── stream-api.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts /.env.exmaple: -------------------------------------------------------------------------------- 1 | VITE_API_DOMAIN='http://xxxxx' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true, node: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:prettier/recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'plugin:tailwindcss/recommended', 9 | ], 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | plugins: ['react-refresh', 'tailwindcss'], 13 | rules: { 14 | 'react-refresh/only-export-components': 'warn', 15 | 'react-hooks/exhaustive-deps': 'off', 16 | '@typescript-eslint/no-var-requires': 'off', 17 | 'tailwindcss/no-custom-classname': 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | .env 25 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint -e $GIT_PARAMS -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | cache 2 | .cache 3 | package.json 4 | package-lock.json 5 | public 6 | CHANGELOG.md 7 | .yarn 8 | dist 9 | node_modules 10 | .next 11 | build 12 | .contentlayer -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'all', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'always', 13 | rangeStart: 0, 14 | rangeEnd: Infinity, 15 | requirePragma: false, 16 | insertPragma: false, 17 | proseWrap: 'preserve', 18 | htmlWhitespaceSensitivity: 'css', 19 | vueIndentScriptAndStyle: false, 20 | endOfLine: 'lf', 21 | }; 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.format.enable": true, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "typescript", 7 | "typescriptreact", 8 | ], 9 | "[vue]": { 10 | "editor.formatOnSave": true, 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[typescriptreact]": { 14 | "editor.formatOnSave": true, 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[javascriptreact]": { 18 | "editor.formatOnSave": true, 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[typescript]": { 22 | "editor.formatOnSave": true, 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[javascript]": { 26 | "editor.formatOnSave": true, 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 GPTLink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

GPTLink Web

3 |

只需简单几步,即可基于 GPTLink 快速搭建你的 ChatGPT 站点。

4 | 5 | [体验地址](https://gptlink-web.vercel.app) · [反馈](https://github.com/gptlink/gptlink-web/issues) · [微信加群](./docs/qrcode.png) 6 | 7 | 8 |
9 | 10 | ## 🎉 特性 11 | 12 | `GPTLink Web` 为 [gptlink](https://github.com/gptlink/gptlink) 项目用户端源码。可将此源码编译后后,替换 `gptlink/gptweb` 目录重新构建镜像,重新构建属于你的专属应用。 13 | 14 | - 接入 [GPTLink](https://gpt-link.com/) 接口, 内置丰富功能 15 | - 采用 `vite` + `react`, 开箱即用的极速开发体验 16 | - 基于 [Tailwind CSS](https://tailwindcss.com/) + [shadcn/ui](https://ui.shadcn.com/) 生态,轻松定制 `UI` 17 | 18 | ## 📦 环境准备 19 | 20 | 1. `node` 建议使用 `^16 || ^18 || ^19` 版本 21 | 22 | 2. 安装 pnpm 包管理工具 23 | 24 | ```shell 25 | npm install pnpm -g 26 | ``` 27 | 28 | ## 🔨使用 29 | 30 | - 获取项目代码 31 | 32 | ```shell 33 | git clone https://github.com/gptlink/gptlink-web 34 | 35 | cd gptlink-web 36 | 37 | * 安装依赖 38 | pnpm install 39 | 40 | # 本地调试,构建命令 pnpm run build 41 | pnpm run dev 42 | ``` 43 | 44 | ## ✅ 版本计划 45 | 46 | - [x] 导出聊天分享图片 47 | - [x] 移动端适配 48 | 49 | ## 项目 50 | 51 | ### 项目配置 52 | 53 | 项目配置文件位于根目录 `.env` ,如若不存在此文件,将 `.env.example` 复制一份更名为 `.env` 作为配置项进行使用,详细配置如下: 54 | 55 | | 变量名称 | 示例 | 说明 | 56 | | --------------------- | --------------------------- | ---------------- | 57 | | VITE_APP_API_BASE_URL | `http://127.0.0.1` | 接口基础请求地址 | 58 | 59 | ## 参与贡献 60 | 61 | 贡献之前请先阅读 [贡献指南](https://github.com/gptlink/gptlink/blob/master/CONTRIBUTING.md) 62 | 63 | 我们深知这不是一个完美的产品,但是它只是一个开始,欢迎加入我们一起完善! 64 | 65 | ## License 66 | 67 | [MIT](./LICENSE) 68 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptlink/gptlink-web/34fac1b8850e9cd2bc6979ec35800f7e6cd2b4bc/docs/banner.png -------------------------------------------------------------------------------- /docs/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptlink/gptlink-web/34fac1b8850e9cd2bc6979ec35800f7e6cd2b4bc/docs/qrcode.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gptlink-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 9 | "preview": "vite preview", 10 | "prettier:write": "prettier --write \"**/*.{ts,tsx,html}\" --cache", 11 | "prepare": "husky install" 12 | }, 13 | "dependencies": { 14 | "@iconify/react": "^4.1.0", 15 | "@radix-ui/react-alert-dialog": "^1.0.4", 16 | "@radix-ui/react-avatar": "^1.0.3", 17 | "@radix-ui/react-checkbox": "^1.0.4", 18 | "@radix-ui/react-dialog": "^1.0.4", 19 | "@radix-ui/react-dropdown-menu": "^2.0.5", 20 | "@radix-ui/react-label": "^2.0.1", 21 | "@radix-ui/react-radio-group": "^1.1.3", 22 | "@radix-ui/react-scroll-area": "^1.0.4", 23 | "@radix-ui/react-separator": "^1.0.3", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "@radix-ui/react-tabs": "^1.0.4", 26 | "@radix-ui/react-tooltip": "^1.0.6", 27 | "@types/js-cookie": "^3.0.3", 28 | "dayjs": "^1.11.7", 29 | "file-saver": "^2.0.5", 30 | "html-to-image": "^1.11.11", 31 | "i18next": "^22.5.0", 32 | "lodash-es": "^4.17.21", 33 | "lucide-react": "^0.233.0", 34 | "qrcode.react": "^3.1.0", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-hook-form": "^7.44.3", 38 | "react-hot-toast": "^2.4.1", 39 | "react-i18next": "^12.3.1", 40 | "react-markdown": "^8.0.7", 41 | "react-router-dom": "^6.11.2", 42 | "rehype-highlight": "^6.0.0", 43 | "rehype-katex": "^6.0.3", 44 | "rehype-raw": "^6.1.1", 45 | "remark-breaks": "^3.0.3", 46 | "remark-gfm": "^3.0.1", 47 | "remark-math": "^5.1.1", 48 | "tailwindcss": "^3.3.2", 49 | "tailwindcss-animate": "^1.0.5", 50 | "zustand": "^4.3.8" 51 | }, 52 | "devDependencies": { 53 | "@commitlint/cli": "^17.6.5", 54 | "@commitlint/config-conventional": "^17.6.5", 55 | "@hookform/resolvers": "^3.1.0", 56 | "@tailwindcss/typography": "^0.5.9", 57 | "@types/file-saver": "^2.0.5", 58 | "@types/lodash-es": "^4.17.7", 59 | "@types/react": "^18.0.37", 60 | "@types/react-dom": "^18.0.11", 61 | "@types/uuid": "^9.0.2", 62 | "@typescript-eslint/eslint-plugin": "^5.59.0", 63 | "@typescript-eslint/parser": "^5.59.0", 64 | "@vitejs/plugin-react-swc": "^3.0.0", 65 | "autoprefixer": "^10.4.14", 66 | "class-variance-authority": "^0.6.0", 67 | "classnames": "^2.3.2", 68 | "clsx": "^1.2.1", 69 | "commitizen": "^4.3.0", 70 | "cz-conventional-changelog": "^3.3.0", 71 | "eslint": "^8.38.0", 72 | "eslint-config-prettier": "^8.8.0", 73 | "eslint-plugin-prettier": "^4.2.1", 74 | "eslint-plugin-react": "^7.32.2", 75 | "eslint-plugin-react-hooks": "^4.6.0", 76 | "eslint-plugin-react-refresh": "^0.3.4", 77 | "eslint-plugin-tailwindcss": "^3.12.1", 78 | "husky": "^8.0.3", 79 | "less": "^4.1.3", 80 | "lint-staged": "^13.2.2", 81 | "postcss": "^8.4.24", 82 | "react-infinite-scroll-component": "^6.1.0", 83 | "tailwind-merge": "^1.13.0", 84 | "typescript": "^5.0.2", 85 | "uuid": "^9.0.0", 86 | "vite": "^4.3.9", 87 | "weixin-js-sdk-ts": "^1.6.1", 88 | "zod": "^3.21.4" 89 | }, 90 | "config": { 91 | "commitizen": { 92 | "path": "./node_modules/cz-conventional-changelog" 93 | } 94 | }, 95 | "lint-staged": { 96 | "*.{js,jsx,ts,tsx}": [ 97 | "npm run prettier:write", 98 | "npm run lint --fix", 99 | "git add ." 100 | ] 101 | } 102 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/api/app.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | import wx from 'weixin-js-sdk-ts'; 3 | 4 | export interface ConfigAgreementType { 5 | title: string; 6 | agreement: string; 7 | enable: boolean; 8 | } 9 | 10 | export enum LoginTypeEnum { 11 | PASSWORD = '1', 12 | WECHAT = '2', 13 | PHONE = '3', 14 | WECHAT_AND_PHONE = '4', 15 | } 16 | 17 | export interface AppConfigInterface { 18 | name: string; 19 | icp: string; 20 | web_logo: string; 21 | admin_logo: string; 22 | user_logo: string; 23 | } 24 | 25 | export interface LoginTypeInterface { 26 | login_type: LoginTypeEnum; 27 | mobile_verify: boolean; 28 | } 29 | 30 | export enum PaymentChannelEnum { 31 | OFFLINE = 'offline', 32 | WECHAT = 'wechat', 33 | } 34 | 35 | export interface PaymentConfigType { 36 | channel: PaymentChannelEnum; 37 | offline: string; 38 | } 39 | 40 | export interface AppConfigType extends LoginTypeInterface, AppConfigInterface, PaymentConfigType {} 41 | 42 | export interface JsSDKType { 43 | debug: boolean; 44 | beta: boolean; 45 | jsApiList: wx.jsApiList; 46 | openTagList: wx.openTagList; 47 | appId: string; 48 | nonceStr: string; 49 | timestamp: number; 50 | url: string; 51 | signature: string; 52 | } 53 | 54 | export interface ShareConfigType { 55 | desc: string; 56 | title: string; 57 | img_url: string; 58 | share_img: string; 59 | } 60 | 61 | export default { 62 | getConfigAgreement(): Promise { 63 | return request('config/agreement'); 64 | }, 65 | getAppConfig(): Promise { 66 | return request('config/basic-info'); 67 | }, 68 | getLoginType(): Promise { 69 | return request('config/login-type'); 70 | }, 71 | getJsSDK(url: string): Promise<{ 72 | data: JsSDKType; 73 | }> { 74 | return request(`wechat/jssdk?url=${encodeURIComponent(url)}`); 75 | }, 76 | getShareConfig(): Promise { 77 | return request('config/share'); 78 | }, 79 | getPaymentConfig(): Promise { 80 | return request('config/payment'); 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /src/api/billing.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export enum Channel { 4 | WECHAT = 'wechat', 5 | ALI_PAY = 'alipay', 6 | } 7 | 8 | export enum PayStatus { 9 | PENDING = 1, 10 | SUCCESS = 2, 11 | } 12 | 13 | export enum PayType { 14 | JSAPI = 'JSAPI', 15 | NATIVE = 'NATIVE', 16 | } 17 | 18 | export interface ExchangeRedemptionCodeType { 19 | id: number; 20 | name: string; 21 | type: number; 22 | expired_day: number; 23 | num: number; 24 | price: number; 25 | } 26 | 27 | export interface PackageType { 28 | id: number; 29 | name: string; 30 | price: number; 31 | identity: number; 32 | } 33 | 34 | export interface PayInfoType { 35 | id: number; 36 | price: string; 37 | channel: Channel; 38 | pay_type: PayType; 39 | package_name: string; 40 | data: { 41 | code_url: string; 42 | appId: string; 43 | timeStamp: string; 44 | nonceStr: string; 45 | signType: string; 46 | paySign: string; 47 | package: string; 48 | }; 49 | } 50 | 51 | export interface OrderBillingType { 52 | id: number; 53 | package_id: number; 54 | channel: Channel; 55 | pay_type: PayType; 56 | user_id: string; 57 | status: PayStatus; 58 | platform: string; 59 | business_id: string; 60 | } 61 | 62 | export interface BillingDetailType { 63 | id: number; 64 | trade_no: string; 65 | user_id: number; 66 | status: number; 67 | } 68 | 69 | export default { 70 | getBillingPackage(): Promise { 71 | return request('package'); 72 | }, 73 | exchangeRedemptionCode(cdk: string): Promise { 74 | return request('cdk', { 75 | method: 'post', 76 | body: JSON.stringify({ cdk }), 77 | }); 78 | }, 79 | orderBilling(data: { 80 | channel: Channel; 81 | package_id: number; 82 | pay_type: PayType; 83 | platform: number; 84 | }): Promise { 85 | return request('order', { 86 | method: 'post', 87 | body: JSON.stringify(data), 88 | }); 89 | }, 90 | billingPayDetail(id: number): Promise { 91 | return request(`order/${id}/pay`); 92 | }, 93 | billingDetail(id: number): Promise { 94 | return request(`order/${id}`); 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /src/api/chat.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export interface RoleType { 4 | id: string; 5 | icon: string; 6 | name: string; 7 | prompt: string; 8 | desc: string; 9 | } 10 | 11 | export default { 12 | getRoleList(): Promise { 13 | return request(`chat-gpt-model?platform=1&is_all=true`); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/api/salesman.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export interface SalesmanChildType { 4 | nickname?: string; 5 | order_price: string; 6 | order_num: number; 7 | created_at: string; 8 | } 9 | 10 | export interface SalesmanOrderType { 11 | price: string; 12 | created_at: string; 13 | custom: { 14 | nickname: string; 15 | }; 16 | } 17 | 18 | export interface SalesmanStatisticsType { 19 | order_num: number; 20 | order_price: string; 21 | custom_num: number; 22 | balance: string; 23 | ratio: number; 24 | } 25 | 26 | export interface SalesmanWithdrawalLastType { 27 | config: { 28 | name: string; 29 | account: string; 30 | }; 31 | } 32 | 33 | export interface SalesmanWithdrawalType { 34 | id: number; 35 | serial_no: string; 36 | price: string; 37 | status: number; 38 | paid_no: string; 39 | user_id: number; 40 | created_at: string; 41 | } 42 | 43 | export default { 44 | getSalesmanChildList(per_page?: number, page?: number): Promise { 45 | return request(`/salesman/child?per_page=${per_page}&page=${page}`); 46 | }, 47 | getSalesmanOrderList(per_page?: number, page?: number): Promise { 48 | return request(`/salesman/order?per_page=${per_page}&page=${page}`); 49 | }, 50 | getSalesmanStatistics(): Promise { 51 | return request(`/salesman/statistics`); 52 | }, 53 | withdrawal(data: { price: string; channel: string; config: { account: string; name: string } }): Promise { 54 | return request(`/salesman/withdrawal/apply`, { 55 | method: 'post', 56 | body: JSON.stringify(data), 57 | }); 58 | }, 59 | getSalesmanWithdrawalLast(): Promise { 60 | return request(`/salesman/withdrawal/last`); 61 | }, 62 | getSalesmanWithdrawalList(per_page?: number, page?: number): Promise { 63 | return request(`/salesman/withdrawal?per_page=${per_page}&page=${page}`); 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /src/api/task.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export enum TaskTypeEnums { 4 | REGISTER = 'register', 5 | INVITE = 'invite', 6 | SHARE = 'share', 7 | SALESMAN = 'salesman', 8 | } 9 | 10 | export interface TaskType { 11 | id: number; 12 | type: TaskTypeEnums; 13 | title: string; 14 | desc: string; 15 | is_completed: boolean; 16 | is_subscribe: boolean; 17 | model_count: 0; 18 | } 19 | 20 | export interface SalesmanConfigType { 21 | enable: boolean; 22 | open: boolean; 23 | rules: string; 24 | } 25 | 26 | export default { 27 | getTaskList(platform: number): Promise { 28 | return request(`task?platform=${platform}`); 29 | }, 30 | checkTask(type: TaskTypeEnums): Promise<{ result: boolean }> { 31 | return request('task/check', { method: 'post', body: JSON.stringify({ type }) }); 32 | }, 33 | completionTask(type: TaskTypeEnums): Promise<{ result: boolean }> { 34 | return request('task/completion', { method: 'post', body: JSON.stringify({ type }) }); 35 | }, 36 | getUnreadTaskList(type: TaskTypeEnums): Promise<{ 37 | type: TaskTypeEnums; 38 | expired_day: number; 39 | package_name: string; 40 | num: number; 41 | record_count: number; 42 | }> { 43 | return request(`/task/record/unread?type=${type}`); 44 | }, 45 | readTask(type: TaskTypeEnums): Promise<{ 46 | type: TaskTypeEnums; 47 | expired_day: string; 48 | package_name: string; 49 | num: number; 50 | record_count: number; 51 | }> { 52 | return request(`task/record/${type}/read`, { method: 'put' }); 53 | }, 54 | getSalesmanConfig(): Promise { 55 | return request(`config/salesman`); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | export interface BillType { 4 | name: string; 5 | num: number; 6 | used: number; 7 | expired_at?: string; 8 | } 9 | 10 | export interface UserPackageType { 11 | num: number; 12 | package_name: string; 13 | created_at: string; 14 | type: number; 15 | expired_day: number; 16 | } 17 | 18 | export interface UserInfoType { 19 | nickname: string; 20 | avatar: string; 21 | identity: number; 22 | openid: string; 23 | } 24 | 25 | export interface UserInfoByCodeType { 26 | oauth_id: string; 27 | user: UserInfoType; 28 | access_token: string; 29 | } 30 | 31 | export interface WxQrCodeType { 32 | qr_code_url: string; 33 | } 34 | 35 | export default { 36 | getWxQrCode(type: string, redirectUrl: string): Promise { 37 | return request(`wechat/${type}/qrcode?type=${type}&redirect_url=${redirectUrl}`); 38 | }, 39 | getUserInfoByCode(type: string, code: string, shareOpenid: string): Promise { 40 | return request(`wechat/${type}/login`, { 41 | method: 'post', 42 | body: JSON.stringify({ code, type, share_openid: shareOpenid }), 43 | }); 44 | }, 45 | getUserProfile(): Promise { 46 | return request('user/profile'); 47 | }, 48 | getUserBill(): Promise { 49 | return request('user/bill-package'); 50 | }, 51 | getUserPackages(): Promise { 52 | return request('user/package/record'); 53 | }, 54 | register(data: { nickname: string; mobile: string; password: string; share_openid: string; code: string }): Promise<{ 55 | user: UserInfoType; 56 | access_token: string; 57 | }> { 58 | return request('auth/register', { 59 | method: 'post', 60 | body: JSON.stringify(data), 61 | }); 62 | }, 63 | login(data: { nickname: string; password: string }): Promise<{ user: UserInfoType; access_token: string }> { 64 | return request('auth/login', { 65 | method: 'post', 66 | body: JSON.stringify(data), 67 | }); 68 | }, 69 | getPhoneCode(mobile: string): Promise<{ data: string }> { 70 | return request('sms/send-code', { 71 | method: 'post', 72 | body: JSON.stringify({ mobile }), 73 | }); 74 | }, 75 | phoneLogin(data: { 76 | mobile: string; 77 | code: string; 78 | oauth_id: string; 79 | share_openid: string; 80 | }): Promise<{ user: UserInfoType; access_token: string }> { 81 | return request('sms/login', { 82 | method: 'post', 83 | body: JSON.stringify(data), 84 | }); 85 | }, 86 | resetPassword(data: { 87 | nickname: string; 88 | password: string; 89 | mobile: string; 90 | reenteredPassword: string; 91 | verify: string; 92 | verify_type: number; 93 | code: string; 94 | }): Promise { 95 | return request('auth/reset', { 96 | method: 'post', 97 | body: JSON.stringify(data), 98 | }); 99 | }, 100 | becomeSalesman(): Promise { 101 | return request('/user/salesman', { 102 | method: 'post', 103 | }); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /src/assets/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptlink/gptlink-web/34fac1b8850e9cd2bc6979ec35800f7e6cd2b4bc/src/assets/poster.png -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | const Icon = ({ className = '' }) => { 2 | return ( 3 | 4 | 10 | 11 | ); 12 | }; 13 | 14 | export default Icon; 15 | -------------------------------------------------------------------------------- /src/components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, ReactNode } from 'react'; 2 | import { Copy } from 'lucide-react'; 3 | import toast from 'react-hot-toast'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import RehypeHighlight from 'rehype-highlight'; 6 | import RehypeKatex from 'rehype-katex'; 7 | import RemarkBreaks from 'remark-breaks'; 8 | import RemarkGfm from 'remark-gfm'; 9 | import RemarkMath from 'remark-math'; 10 | import rehypeRaw from 'rehype-raw'; 11 | 12 | import { Button } from '@/components/ui/button'; 13 | import { copyToClipboard } from '@/utils'; 14 | 15 | export function PreCode(props: { children: ReactNode }) { 16 | const ref = useRef(null); 17 | 18 | return ( 19 |
20 |       
34 |       {props.children}
35 |     
36 | ); 37 | } 38 | 39 | export function Markdown(props: { content: string }) { 40 | return ( 41 | 46 | {props.content} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/SvgIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react'; 2 | 3 | const SvgIcon = ({ icon = '', className = '' }) => { 4 | return ; 5 | }; 6 | 7 | export default SvgIcon; 8 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; 5 | 6 | import { cn } from '@/utils'; 7 | import { buttonVariants } from '@/components/ui/button'; 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root; 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 12 | 13 | const AlertDialogPortal = ({ className, children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => ( 14 | 15 |
{children}
16 |
17 | ); 18 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; 19 | 20 | const AlertDialogOverlay = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, ...props }, ref) => ( 24 | 32 | )); 33 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 34 | 35 | const AlertDialogContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 40 | 41 | 49 | 50 | )); 51 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 52 | 53 | const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 54 |
55 | ); 56 | AlertDialogHeader.displayName = 'AlertDialogHeader'; 57 | 58 | const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 59 |
60 | ); 61 | AlertDialogFooter.displayName = 'AlertDialogFooter'; 62 | 63 | const AlertDialogTitle = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, ...props }, ref) => ( 67 | 68 | )); 69 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 70 | 71 | const AlertDialogDescription = React.forwardRef< 72 | React.ElementRef, 73 | React.ComponentPropsWithoutRef 74 | >(({ className, ...props }, ref) => ( 75 | 76 | )); 77 | AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; 78 | 79 | const AlertDialogAction = React.forwardRef< 80 | React.ElementRef, 81 | React.ComponentPropsWithoutRef 82 | >(({ className, ...props }, ref) => ( 83 | 84 | )); 85 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 86 | 87 | const AlertDialogCancel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => ( 91 | 96 | )); 97 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 98 | 99 | export { 100 | AlertDialog, 101 | AlertDialogTrigger, 102 | AlertDialogContent, 103 | AlertDialogHeader, 104 | AlertDialogFooter, 105 | AlertDialogTitle, 106 | AlertDialogDescription, 107 | AlertDialogAction, 108 | AlertDialogCancel, 109 | }; 110 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 5 | 6 | import { cn } from '@/utils'; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 17 | )); 18 | Avatar.displayName = AvatarPrimitive.Root.displayName; 19 | 20 | const AvatarImage = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, ...props }, ref) => ( 24 | 25 | )); 26 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 27 | 28 | const AvatarFallback = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 37 | )); 38 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 39 | 40 | export { Avatar, AvatarImage, AvatarFallback }; 41 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import * as React from 'react'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | 6 | import { cn } from '@/utils'; 7 | 8 | const buttonVariants = cva( 9 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 10 | { 11 | variants: { 12 | variant: { 13 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 14 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 'border border-input hover:bg-accent hover:text-accent-foreground', 16 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 17 | ghost: 'hover:bg-accent hover:text-accent-foreground', 18 | link: 'text-primary underline-offset-4 hover:underline', 19 | }, 20 | size: { 21 | default: 'h-10 px-4 py-2', 22 | sm: 'h-9 rounded-md px-3', 23 | lg: 'h-11 rounded-md px-8', 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: 'default', 28 | size: 'default', 29 | }, 30 | }, 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : 'button'; 42 | return ; 43 | }, 44 | ); 45 | Button.displayName = 'Button'; 46 | 47 | export { Button, buttonVariants }; 48 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/utils'; 4 | 5 | const Card = React.forwardRef>(({ className, ...props }, ref) => ( 6 |
7 | )); 8 | Card.displayName = 'Card'; 9 | 10 | const CardHeader = React.forwardRef>( 11 | ({ className, ...props }, ref) => ( 12 |
13 | ), 14 | ); 15 | CardHeader.displayName = 'CardHeader'; 16 | 17 | const CardTitle = React.forwardRef>( 18 | ({ className, ...props }, ref) => ( 19 |

20 | ), 21 | ); 22 | CardTitle.displayName = 'CardTitle'; 23 | 24 | const CardDescription = React.forwardRef>( 25 | ({ className, ...props }, ref) => ( 26 |

27 | ), 28 | ); 29 | CardDescription.displayName = 'CardDescription'; 30 | 31 | const CardContent = React.forwardRef>( 32 | ({ className, ...props }, ref) =>

, 33 | ); 34 | CardContent.displayName = 'CardContent'; 35 | 36 | const CardFooter = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ); 41 | CardFooter.displayName = 'CardFooter'; 42 | 43 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; 44 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { Check } from 'lucide-react'; 6 | 7 | import { cn } from '@/utils'; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 22 | 23 | 24 | 25 | )); 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 27 | 28 | export { Checkbox }; 29 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 5 | import { X } from 'lucide-react'; 6 | 7 | import { cn } from '@/utils'; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => ( 14 | 15 |
{children}
16 |
17 | ); 18 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 19 | 20 | const DialogOverlay = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef 23 | >(({ className, ...props }, ref) => ( 24 | 32 | )); 33 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 34 | 35 | const DialogContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 | {children} 50 | 51 | 52 | Close 53 | 54 | 55 | 56 | )); 57 | DialogContent.displayName = DialogPrimitive.Content.displayName; 58 | 59 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 60 |
61 | ); 62 | DialogHeader.displayName = 'DialogHeader'; 63 | 64 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 65 |
66 | ); 67 | DialogFooter.displayName = 'DialogFooter'; 68 | 69 | const DialogTitle = React.forwardRef< 70 | React.ElementRef, 71 | React.ComponentPropsWithoutRef 72 | >(({ className, ...props }, ref) => ( 73 | 78 | )); 79 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 80 | 81 | const DialogDescription = React.forwardRef< 82 | React.ElementRef, 83 | React.ComponentPropsWithoutRef 84 | >(({ className, ...props }, ref) => ( 85 | 86 | )); 87 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 88 | 89 | export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; 90 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 5 | import { Check, ChevronRight, Circle } from 'lucide-react'; 6 | 7 | import { cn } from '@/utils'; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; 41 | 42 | const DropdownMenuSubContent = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ className, ...props }, ref) => ( 46 | 54 | )); 55 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 71 | 72 | )); 73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 74 | 75 | const DropdownMenuItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef & { 78 | inset?: boolean; 79 | } 80 | >(({ className, inset, ...props }, ref) => ( 81 | 90 | )); 91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 92 | 93 | const DropdownMenuCheckboxItem = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, children, checked, ...props }, ref) => ( 97 | 106 | 107 | 108 | 109 | 110 | 111 | {children} 112 | 113 | )); 114 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; 115 | 116 | const DropdownMenuRadioItem = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, children, ...props }, ref) => ( 120 | 128 | 129 | 130 | 131 | 132 | 133 | {children} 134 | 135 | )); 136 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 137 | 138 | const DropdownMenuLabel = React.forwardRef< 139 | React.ElementRef, 140 | React.ComponentPropsWithoutRef & { 141 | inset?: boolean; 142 | } 143 | >(({ className, inset, ...props }, ref) => ( 144 | 149 | )); 150 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 151 | 152 | const DropdownMenuSeparator = React.forwardRef< 153 | React.ElementRef, 154 | React.ComponentPropsWithoutRef 155 | >(({ className, ...props }, ref) => ( 156 | 157 | )); 158 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 159 | 160 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 161 | return ; 162 | }; 163 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; 164 | 165 | export { 166 | DropdownMenu, 167 | DropdownMenuTrigger, 168 | DropdownMenuContent, 169 | DropdownMenuItem, 170 | DropdownMenuCheckboxItem, 171 | DropdownMenuRadioItem, 172 | DropdownMenuLabel, 173 | DropdownMenuSeparator, 174 | DropdownMenuShortcut, 175 | DropdownMenuGroup, 176 | DropdownMenuPortal, 177 | DropdownMenuSub, 178 | DropdownMenuSubContent, 179 | DropdownMenuSubTrigger, 180 | DropdownMenuRadioGroup, 181 | }; 182 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as LabelPrimitive from '@radix-ui/react-label'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from 'react-hook-form'; 5 | 6 | import { cn } from '@/utils'; 7 | import { Label } from '@/components/ui/label'; 8 | 9 | const Form = FormProvider; 10 | 11 | type FormFieldContextValue< 12 | TFieldValues extends FieldValues = FieldValues, 13 | TName extends FieldPath = FieldPath, 14 | > = { 15 | name: TName; 16 | }; 17 | 18 | const FormFieldContext = React.createContext({} as FormFieldContextValue); 19 | 20 | const FormField = < 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath, 23 | >({ 24 | ...props 25 | }: ControllerProps) => { 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | const useFormField = () => { 34 | const fieldContext = React.useContext(FormFieldContext); 35 | const itemContext = React.useContext(FormItemContext); 36 | const { getFieldState, formState } = useFormContext(); 37 | 38 | const fieldState = getFieldState(fieldContext.name, formState); 39 | 40 | if (!fieldContext) { 41 | throw new Error('useFormField should be used within '); 42 | } 43 | 44 | const { id } = itemContext; 45 | 46 | return { 47 | id, 48 | name: fieldContext.name, 49 | formItemId: `${id}-form-item`, 50 | formDescriptionId: `${id}-form-item-description`, 51 | formMessageId: `${id}-form-item-message`, 52 | ...fieldState, 53 | }; 54 | }; 55 | 56 | type FormItemContextValue = { 57 | id: string; 58 | }; 59 | 60 | const FormItemContext = React.createContext({} as FormItemContextValue); 61 | 62 | const FormItem = React.forwardRef>( 63 | ({ className, ...props }, ref) => { 64 | const id = React.useId(); 65 | 66 | return ( 67 | 68 |
69 | 70 | ); 71 | }, 72 | ); 73 | FormItem.displayName = 'FormItem'; 74 | 75 | const FormLabel = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, ...props }, ref) => { 79 | const { error, formItemId } = useFormField(); 80 | 81 | return