mirror of
https://github.com/coaidev/coai.git
synced 2025-05-21 14:00:13 +09:00
update mobile adapter and store
This commit is contained in:
parent
3e03da83c6
commit
fcec3d8a4a
@ -61,7 +61,7 @@ export const modelColorMapper: Record<string, string> = {
|
||||
"code-llama-13b": "#01a9f0",
|
||||
"code-llama-7b": "#01a9f0",
|
||||
|
||||
"hunyuan": "#0052d9"
|
||||
hunyuan: "#0052d9",
|
||||
};
|
||||
|
||||
export function getModelColor(model: string): string {
|
||||
|
@ -23,7 +23,7 @@
|
||||
height: max-content;
|
||||
padding: 1rem 2rem;
|
||||
|
||||
@media (max-width: 668px) {
|
||||
@media (max-width: 940px) {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
|
||||
@ -103,6 +103,7 @@
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 1rem 1.5rem;
|
||||
width: 100%;
|
||||
|
||||
.chart-box {
|
||||
width: calc(50% - 1rem);
|
||||
@ -116,5 +117,13 @@
|
||||
border: 1px solid hsl(var(--border));
|
||||
user-select: none;
|
||||
box-shadow: 0 0 1rem 0 hsla(var(--foreground), 0.1);
|
||||
|
||||
@media (max-width: 680px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -36,12 +36,35 @@
|
||||
border-radius: var(--radius);
|
||||
font-size: 16px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 2rem;
|
||||
background: hsl(var(--text));
|
||||
border-radius: var(--radius);
|
||||
margin-right: 0;
|
||||
opacity: 0;
|
||||
transition: 0.25s;
|
||||
transition-property: opacity, margin-right;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--conversation-card-hover);
|
||||
|
||||
&:before {
|
||||
opacity: .25;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--conversation-card-active);
|
||||
|
||||
&:before {
|
||||
opacity: 1;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
|
@ -32,6 +32,7 @@
|
||||
|
||||
--border: 37 26% 83%;
|
||||
--border-hover: 37 26% 78%;
|
||||
--border-active: 37 26% 73%;
|
||||
--input: 37 26% 90%;
|
||||
--input-unread: 37 26% 70%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
@ -80,6 +81,7 @@
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--border-hover: 240 3.7% 20.9%;
|
||||
--border-active: 240 3.7% 25.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--input-unread: 240 3.7% 50%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
@ -122,11 +122,11 @@
|
||||
max-width: 100%;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border: 1px solid hsl(var(--border-hover));
|
||||
transition: 0.25s linear;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--border-hover));
|
||||
border-color: hsl(var(--border-active));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectMenu } from "@/store/menu.ts";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { closeMenu, selectMenu } from "@/store/menu.ts";
|
||||
import React, { useMemo } from "react";
|
||||
import { LayoutDashboard, Settings, Users } from "lucide-react";
|
||||
import router from "@/router.tsx";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mobile } from "@/utils/device.ts";
|
||||
|
||||
type MenuItemProps = {
|
||||
title: string;
|
||||
@ -14,6 +15,7 @@ type MenuItemProps = {
|
||||
|
||||
function MenuItem({ title, icon, path }: MenuItemProps) {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const active = useMemo(
|
||||
() =>
|
||||
location.pathname === `/admin${path}` ||
|
||||
@ -21,11 +23,13 @@ function MenuItem({ title, icon, path }: MenuItemProps) {
|
||||
[location.pathname, path],
|
||||
);
|
||||
|
||||
const redirect = async () => {
|
||||
if (mobile) dispatch(closeMenu());
|
||||
await router.navigate(`/admin${path}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`menu-item ${active ? "active" : ""}`}
|
||||
onClick={() => router.navigate(`/admin${path}`)}
|
||||
>
|
||||
<div className={`menu-item ${active ? "active" : ""}`} onClick={redirect}>
|
||||
<div className={`menu-item-icon`}>{icon}</div>
|
||||
<div className={`menu-item-title`}>{title}</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { logout, selectUsername } from "@/store/auth.ts";
|
||||
import { logout, selectAdmin, selectUsername } from "@/store/auth.ts";
|
||||
import { openDialog as openQuotaDialog, quotaSelector } from "@/store/quota.ts";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -19,12 +19,14 @@ import {
|
||||
Gift,
|
||||
ListStart,
|
||||
Plug,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import { openDialog as openSub } from "@/store/subscription.ts";
|
||||
import { openDialog as openPackageDialog } from "@/store/package.ts";
|
||||
import { openDialog as openInvitationDialog } from "@/store/invitation.ts";
|
||||
import { openDialog as openSharingDialog } from "@/store/sharing.ts";
|
||||
import { openDialog as openApiDialog } from "@/store/api.ts";
|
||||
import router from "@/router.tsx";
|
||||
|
||||
type MenuBarProps = {
|
||||
children: React.ReactNode;
|
||||
@ -36,6 +38,7 @@ function MenuBar({ children, className }: MenuBarProps) {
|
||||
const dispatch = useDispatch();
|
||||
const username = useSelector(selectUsername);
|
||||
const quota = useSelector(quotaSelector);
|
||||
const admin = useSelector(selectAdmin);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@ -71,6 +74,12 @@ function MenuBar({ children, className }: MenuBarProps) {
|
||||
<Plug className={`h-4 w-4 mr-1`} />
|
||||
{t("api.title")}
|
||||
</DropdownMenuItem>
|
||||
{admin && (
|
||||
<DropdownMenuItem onClick={() => router.navigate("/admin")}>
|
||||
<Shield className={`h-4 w-4 mr-1`} />
|
||||
{t("admin.users")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
FolderKanban,
|
||||
Link,
|
||||
Newspaper,
|
||||
Shield,
|
||||
Users2,
|
||||
} from "lucide-react";
|
||||
import router from "@/router.tsx";
|
||||
@ -21,10 +20,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { getLanguage } from "@/i18n.ts";
|
||||
import { selectAdmin } from "@/store/auth.ts";
|
||||
|
||||
function ChatSpace() {
|
||||
const admin = useSelector(selectAdmin);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const subscription = useSelector(isSubscribedSelector);
|
||||
@ -85,12 +82,6 @@ function ChatSpace() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className={`space-footer`}>
|
||||
{admin && (
|
||||
<p>
|
||||
<Shield className={`h-3 w-3 mr-1`} />
|
||||
<a onClick={() => router.navigate("/admin")}>{t("admin.users")}</a>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<Link className={`h-3 w-3 mr-1`} />
|
||||
<a
|
||||
|
@ -14,24 +14,22 @@ export function parseProgressbar(data: string) {
|
||||
<p className={`text-primary select-none text-center`}>
|
||||
Generating: {progress < 0 ? 0 : progress.toFixed()}%
|
||||
</p>
|
||||
{
|
||||
progress > 0 && (
|
||||
<div
|
||||
className={`progressbar relative h-4 w-full overflow-hidden rounded-full bg-muted min-w-[20vw]`}
|
||||
role={`progressbar`}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={progress}
|
||||
{progress > 0 && (
|
||||
<div
|
||||
className={`progressbar relative h-4 w-full overflow-hidden rounded-full bg-muted min-w-[20vw]`}
|
||||
role={`progressbar`}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={progress}
|
||||
data-max={100}
|
||||
>
|
||||
<p
|
||||
className={`h-full w-full flex-1 bg-primary transition-all`}
|
||||
style={{ transform: `translateX(-${100 - progress}%)` }}
|
||||
data-max={100}
|
||||
>
|
||||
<p
|
||||
className={`h-full w-full flex-1 bg-primary transition-all`}
|
||||
style={{ transform: `translateX(-${100 - progress}%)` }}
|
||||
data-max={100}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from "@/utils/env.ts";
|
||||
import { getMemory } from "@/utils/memory.ts";
|
||||
|
||||
export const version = "3.6.22";
|
||||
export const version = "3.6.23";
|
||||
export const dev: boolean = getDev();
|
||||
export const deploy: boolean = true;
|
||||
export let rest_api: string = getRestApi(deploy);
|
||||
|
@ -14,14 +14,15 @@ import {
|
||||
certSelector,
|
||||
closeDialog,
|
||||
dialogSelector,
|
||||
refreshPackageTask,
|
||||
refreshPackage,
|
||||
setDialog,
|
||||
teenagerSelector,
|
||||
} from "@/store/package.ts";
|
||||
import { useEffect } from "react";
|
||||
import { Gift } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { selectAuthenticated } from "@/store/auth.ts";
|
||||
|
||||
function Package() {
|
||||
const { t } = useTranslation();
|
||||
@ -29,10 +30,15 @@ function Package() {
|
||||
const open = useSelector(dialogSelector);
|
||||
const cert = useSelector(certSelector);
|
||||
const teenager = useSelector(teenagerSelector);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
|
||||
useEffect(() => {
|
||||
refreshPackageTask(dispatch);
|
||||
}, []);
|
||||
useEffectAsync(async () => {
|
||||
if (!auth) return;
|
||||
const task = setInterval(() => refreshPackage(dispatch), 20000);
|
||||
await refreshPackage(dispatch);
|
||||
|
||||
return () => clearInterval(task);
|
||||
}, [auth]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(open) => dispatch(setDialog(open))}>
|
||||
|
@ -2,11 +2,11 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
closeDialog,
|
||||
dialogSelector,
|
||||
refreshQuotaTask,
|
||||
refreshQuota,
|
||||
setDialog,
|
||||
} from "@/store/quota.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -40,6 +40,8 @@ import {
|
||||
import { AlertDialogTitle } from "@radix-ui/react-alert-dialog";
|
||||
import { buyQuota } from "@/conversation/addition.ts";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { selectAuthenticated } from "@/store/auth.ts";
|
||||
|
||||
type AmountComponentProps = {
|
||||
amount: number;
|
||||
@ -78,10 +80,16 @@ function Quota() {
|
||||
const [current, setCurrent] = useState(1);
|
||||
const [amount, setAmount] = useState(10);
|
||||
const open = useSelector(dialogSelector);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
refreshQuotaTask(dispatch);
|
||||
}, []);
|
||||
useEffectAsync(async () => {
|
||||
if (!auth) return;
|
||||
const task = setInterval(() => refreshQuota(dispatch), 5000);
|
||||
await refreshQuota(dispatch);
|
||||
|
||||
return () => clearInterval(task);
|
||||
}, [auth]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
expiredSelector,
|
||||
isSubscribedSelector,
|
||||
refreshSubscription,
|
||||
refreshSubscriptionTask,
|
||||
setDialog,
|
||||
usageSelector,
|
||||
} from "@/store/subscription.ts";
|
||||
@ -20,7 +19,7 @@ import {
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import "@/assets/pages/subscription.less";
|
||||
import {
|
||||
BookText,
|
||||
@ -52,6 +51,8 @@ import {
|
||||
} from "@/components/ui/select.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import { buySubscription } from "@/conversation/addition.ts";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { selectAuthenticated } from "@/store/auth.ts";
|
||||
|
||||
function calc_prize(month: number): number {
|
||||
const base = 32 * month;
|
||||
@ -169,10 +170,16 @@ function Subscription() {
|
||||
const enterprise = useSelector(enterpriseSelector);
|
||||
const expired = useSelector(expiredSelector);
|
||||
const usage = useSelector(usageSelector);
|
||||
const auth = useSelector(selectAuthenticated);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
refreshSubscriptionTask(dispatch);
|
||||
}, []);
|
||||
useEffectAsync(async () => {
|
||||
if (!auth) return;
|
||||
const task = setInterval(() => refreshSubscription(dispatch), 10000);
|
||||
await refreshSubscription(dispatch);
|
||||
|
||||
return () => clearInterval(task);
|
||||
}, [auth]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -7,12 +7,13 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InvitationTable from "@/components/admin/InvitationTable.tsx";
|
||||
import UserTable from "@/components/admin/UserTable.tsx";
|
||||
import {mobile} from "@/utils/device.ts";
|
||||
|
||||
function Users() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`user-interface`}>
|
||||
<div className={`user-interface ${mobile ? 'mobile' : ''}`}>
|
||||
<Card>
|
||||
<CardHeader className={`select-none`}>
|
||||
<CardTitle>{t("admin.users")}</CardTitle>
|
||||
|
@ -42,17 +42,7 @@ 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;
|
||||
|
||||
const refreshPackage = async (dispatch: AppDispatch) => {
|
||||
const current = new Date().getTime(); //@ts-ignore
|
||||
if (window.hasOwnProperty("package") && current - window.package < 2500)
|
||||
return; //@ts-ignore
|
||||
window.package = current;
|
||||
|
||||
export const refreshPackage = async (dispatch: AppDispatch) => {
|
||||
const response = await getPackage();
|
||||
if (response.status) dispatch(refreshState(response));
|
||||
};
|
||||
|
||||
export const refreshPackageTask = (dispatch: AppDispatch) => {
|
||||
setInterval(() => refreshPackage(dispatch), 20000);
|
||||
refreshPackage(dispatch).then();
|
||||
};
|
||||
|
@ -50,16 +50,7 @@ export const quotaValueSelector = (state: RootState): number =>
|
||||
export const quotaSelector = (state: RootState): string =>
|
||||
state.quota.quota.toFixed(2);
|
||||
|
||||
const refreshQuota = async (dispatch: AppDispatch) => {
|
||||
const current = new Date().getTime(); //@ts-ignore
|
||||
if (window.hasOwnProperty("quota") && current - window.quota < 5000) return; //@ts-ignore
|
||||
window.quota = current;
|
||||
|
||||
export const refreshQuota = async (dispatch: AppDispatch) => {
|
||||
const response = await axios.get("/quota");
|
||||
if (response.data.status) dispatch(setQuota(response.data.quota));
|
||||
};
|
||||
|
||||
export const refreshQuotaTask = (dispatch: AppDispatch) => {
|
||||
setInterval(() => refreshQuota(dispatch), 8000);
|
||||
refreshQuota(dispatch).then();
|
||||
};
|
||||
|
@ -55,19 +55,6 @@ export const enterpriseSelector = (state: any): boolean =>
|
||||
state.subscription.enterprise;
|
||||
|
||||
export const refreshSubscription = async (dispatch: AppDispatch) => {
|
||||
const current = new Date().getTime(); //@ts-ignore
|
||||
if (
|
||||
window.hasOwnProperty("subscription") && //@ts-ignore
|
||||
current - window.subscription < 15000
|
||||
)
|
||||
return; //@ts-ignore
|
||||
window.subscription = current;
|
||||
|
||||
const response = await getSubscription();
|
||||
if (response.status) dispatch(updateSubscription(response));
|
||||
};
|
||||
|
||||
export const refreshSubscriptionTask = (dispatch: AppDispatch) => {
|
||||
setInterval(() => refreshSubscription(dispatch), 20000);
|
||||
refreshSubscription(dispatch).then();
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user