diff --git a/api/chat.go b/api/chat.go index bf32e12..8b91056 100644 --- a/api/chat.go +++ b/api/chat.go @@ -60,8 +60,9 @@ func TextChat(db *sql.DB, user *auth.User, conn *websocket.Conn, instance *conve SendSegmentMessage(conn, types.ChatGPTSegmentResponse{ Message: defaultErrorMessage, Quota: buffer.GetQuota(), - End: false, + End: true, }) + return defaultErrorMessage } // collect quota diff --git a/app/src/App.tsx b/app/src/App.tsx index cc327ed..c69bb6a 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -29,6 +29,7 @@ import {login, tokenField} from "./conf.ts"; import { useTranslation } from "react-i18next"; import Quota from "./routes/Quota.tsx"; import {openDialog} from "./store/quota.ts"; +import Package from "./routes/Package.tsx"; function Settings() { const { t } = useTranslation(); @@ -113,6 +114,7 @@ function App() { + ); } diff --git a/app/src/assets/package.less b/app/src/assets/package.less new file mode 100644 index 0000000..e69de29 diff --git a/app/src/assets/quota.less b/app/src/assets/quota.less index 59314ab..2714245 100644 --- a/app/src/assets/quota.less +++ b/app/src/assets/quota.less @@ -30,6 +30,7 @@ .buy-button { width: 100%; + transition: .25s; } } diff --git a/app/src/conversation/addition.ts b/app/src/conversation/addition.ts new file mode 100644 index 0000000..fda333a --- /dev/null +++ b/app/src/conversation/addition.ts @@ -0,0 +1,34 @@ +import axios from "axios"; + +type QuotaResponse = { + status: boolean; + error: string; +} + +type PackageResponse = { + status: boolean; + cert: boolean; + teenager: boolean; +} + +export async function buyQuota( + quota: number, +): Promise { + try { + const resp = await axios.post(`/buy`, { quota }); + return resp.data as QuotaResponse; + } catch (e) { + console.debug(e); + return { status: false, error: "network error" }; + } +} + +export async function getPackage(): Promise { + try { + const resp = await axios.get(`/package`); + return resp.data as PackageResponse; + } catch (e) { + console.debug(e); + return { status: false, cert: false, teenager: false }; + } +} diff --git a/app/src/i18n.ts b/app/src/i18n.ts index e4457cc..c0875a8 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -66,6 +66,23 @@ const resources = { "output": "Output", "tip": "Prices have been aligned (or lower) to OpenAI models", "learn-more": "Learn more", + "dialog-title": "Buy Points", + "dialog-desc": "Are you sure you want to buy {{amount}} points?", + "dialog-cancel": "Cancel", + "dialog-buy": "Buy", + "success": "Purchase successful", + "success-prompt": "You have successfully purchased {{amount}} points.", + "failed": "Purchase failed", + "failed-prompt": "Failed to purchase points. Please make sure you have enough balance, you will soon jump to deeptrain wallet to pay balance.", + }, + pkg: { + "title": "Packages", + "go": "Go to", + "verify": "Verify", + "cert": "Certification Package", + "cert-desc": "After real-name certification, you can get 50 points (worth 5 CNY)", + "teen": "Teenager Package", + "teen-desc": "After real-name certification, teenagers (18 years old and below) can get an additional 150 points (worth 15 CNY)", } }, }, @@ -124,6 +141,23 @@ const resources = { "output": "输出", "tip": "价格已对齐OpenAI模型或低于官方价格", "learn-more": "了解更多", + "dialog-title": "购买点数", + "dialog-desc": "您确定要购买 {{amount}} 点数吗?", + "dialog-cancel": "取消", + "dialog-buy": "购买", + "success": "购买成功", + "success-prompt": "您已成功购买 {{amount}} 点数。", + "failed": "购买失败", + "failed-prompt": "购买点数失败。请确保您有足够的余额,您即将跳转到 deeptrain 钱包支付余额。", + }, + pkg: { + "title": "礼包", + "go": "前往", + "verify": "实名认证", + "cert": "实名认证礼包", + "cert-desc": "实名认证后可获得 50 点数 (价值 5 元)", + "teen": "未成年人福利", + "teen-desc": "实名认证后未成年人(18 周岁及以下)可额外获得 150 点数 (价值 15 元)", } }, }, diff --git a/app/src/routes/Package.tsx b/app/src/routes/Package.tsx new file mode 100644 index 0000000..36de917 --- /dev/null +++ b/app/src/routes/Package.tsx @@ -0,0 +1,41 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../components/ui/dialog.tsx"; +import {Button} from "../components/ui/button.tsx"; +import "../assets/package.less"; +import {useTranslation} from "react-i18next"; +import {useDispatch, useSelector} from "react-redux"; +import {dialogSelector, refreshPackage} from "../store/package.ts"; +import {useEffect} from "react"; + +function Package() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const open = useSelector(dialogSelector); + useEffect(() => { + refreshPackage(dispatch); + }, []); + + return ( + + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + + + + + + + ) +} + +export default Package; diff --git a/app/src/routes/Quota.tsx b/app/src/routes/Quota.tsx index cb3176e..335f8ab 100644 --- a/app/src/routes/Quota.tsx +++ b/app/src/routes/Quota.tsx @@ -1,5 +1,5 @@ import {useDispatch, useSelector} from "react-redux"; -import {dialogSelector, refreshQuota, setDialog} from "../store/quota.ts"; +import {closeDialog, dialogSelector, refreshQuota, setDialog} from "../store/quota.ts"; import {useTranslation} from "react-i18next"; import {useEffect, useState} from "react"; import { @@ -15,6 +15,16 @@ import {Input} from "../components/ui/input.tsx"; import {testNumberInputEvent} from "../utils.ts"; import {Button} from "../components/ui/button.tsx"; import {Separator} from "../components/ui/separator.tsx"; +import { + AlertDialog, AlertDialogAction, AlertDialogCancel, + AlertDialogContent, AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTrigger +} from "../components/ui/alert-dialog.tsx"; +import {AlertDialogTitle} from "@radix-ui/react-alert-dialog"; +import {buyQuota} from "../conversation/addition.ts"; +import {useToast} from "../components/ui/use-toast.ts"; type AmountComponentProps = { amount: number; @@ -45,8 +55,9 @@ function AmountComponent({ amount, active, other, onClick }: AmountComponentProp function Quota() { const { t } = useTranslation(); - const [ current, setCurrent ] = useState(0); - const [ amount, setAmount ] = useState(0); + const { toast } = useToast(); + const [ current, setCurrent ] = useState(1); + const [ amount, setAmount ] = useState(10); const open = useSelector(dialogSelector); const dispatch = useDispatch(); useEffect(() => { @@ -107,10 +118,43 @@ function Quota() { }
- + + + + + + + { t('buy.dialog-title') } + { t('buy.dialog-desc', { amount }) } + + + { t('buy.dialog-cancel') } + { + const res = await buyQuota(amount); + if (res.status) { + toast({ + title: t('buy.success'), + description: t('buy.success-prompt', { amount }), + }) + dispatch(closeDialog()); + } else { + toast({ + title: t('buy.failed'), + description: t('buy.failed-prompt', { amount }), + }) + setTimeout(() => { + window.open('https://deeptrain.lightxi.com/home/wallet'); + }, 2000); + } + } + }>{ t('buy.dialog-buy') } + + +
diff --git a/app/src/store/index.ts b/app/src/store/index.ts index 786d26f..dc0d64c 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -3,6 +3,7 @@ import menuReducer from "./menu"; import authReducer from "./auth"; import chatReducer from "./chat"; import quotaReducer from "./quota"; +import packageReducer from "./package"; const store = configureStore({ reducer: { @@ -10,6 +11,7 @@ const store = configureStore({ auth: authReducer, chat: chatReducer, quota: quotaReducer, + package: packageReducer, }, }); diff --git a/app/src/store/package.ts b/app/src/store/package.ts new file mode 100644 index 0000000..c3c6c0a --- /dev/null +++ b/app/src/store/package.ts @@ -0,0 +1,47 @@ +import {createSlice} from "@reduxjs/toolkit"; +import {getPackage} from "../conversation/addition.ts"; + +export const packageSlice = createSlice({ + name: "package", + initialState: { + dialog: false, + cert: false, + teenager: false, + }, + reducers: { + toggleDialog: (state) => { + state.dialog = !state.dialog; + }, + setDialog: (state, action) => { + state.dialog = action.payload as boolean; + }, + openDialog: (state) => { + state.dialog = true; + }, + closeDialog: (state) => { + state.dialog = false; + }, + refreshState: (state, action) => { + state.cert = action.payload.cert; + state.teenager = action.payload.teenager; + }, + } +}); + +export const {toggleDialog, setDialog, openDialog, closeDialog, refreshState} = packageSlice.actions; +export default packageSlice.reducer; + +export const dialogSelector = (state: any): boolean => state.package.dialog; +export const certSelector = (state: any): boolean => state.package.cert; +export const teenagerSelector = (state: any): boolean => state.package.teenager; + +export const refreshPackage = (dispatch: any) => { + setInterval(async () => { + const current = new Date().getTime(); //@ts-ignore + if (window.hasOwnProperty("package") && (current - window.package < 2500)) return; //@ts-ignore + window.package = current; + + const response = await getPackage(); + if (response.status) dispatch(refreshState(response)); + }, 10000); +} diff --git a/auth/controller.go b/auth/controller.go index 5f5b9b0..44d9db7 100644 --- a/auth/controller.go +++ b/auth/controller.go @@ -75,10 +75,12 @@ func BuyAPI(c *gin.Context) { } money := float32(form.Quota) * 0.1 - if Pay(user.Username, float32(money)*0.1) { + if Pay(user.Username, money) { + user.IncreaseQuota(db, float32(form.Quota)) + c.JSON(200, gin.H{ "status": true, - "data": user.GetQuota(db), + "error": "success", }) } else { c.JSON(200, gin.H{