user timezone

This commit is contained in:
archer
2023-09-05 11:30:52 +08:00
parent 562fd2692d
commit 7a926b7086
18 changed files with 166 additions and 65 deletions

View File

@@ -62,6 +62,7 @@
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
"sass": "^1.58.3",
"timezones-list": "^3.0.2",
"tunnel": "^0.0.6",
"winston": "^3.10.0",
"winston-mongodb": "^5.1.1",

9
client/pnpm-lock.yaml generated
View File

@@ -164,6 +164,9 @@ dependencies:
sass:
specifier: ^1.58.3
version: registry.npmmirror.com/sass@1.58.3
timezones-list:
specifier: ^3.0.2
version: registry.npmmirror.com/timezones-list@3.0.2
tunnel:
specifier: ^0.0.6
version: registry.npmmirror.com/tunnel@0.0.6
@@ -11732,6 +11735,12 @@ packages:
version: 0.2.0
dev: true
registry.npmmirror.com/timezones-list@3.0.2:
resolution: {integrity: sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/timezones-list/-/timezones-list-3.0.2.tgz}
name: timezones-list
version: 3.0.2
dev: false
registry.npmmirror.com/tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz}
name: tiny-invariant

View File

@@ -184,6 +184,7 @@
"Sign Out": "Sign Out",
"Source": "Source",
"Time": "Time",
"Timezone": "Timezone",
"Total Amount": "Total Amount",
"Update Password": "Update Password",
"Update password failed": "Update password failed",

View File

@@ -184,6 +184,7 @@
"Sign Out": "登出",
"Source": "来源",
"Time": "时间",
"Timezone": "时区",
"Total Amount": "总金额",
"Update Password": "修改密码",
"Update password failed": "修改密码异常",

View File

@@ -37,7 +37,7 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useRouter } from 'next/router';
import { useGlobalStore } from '@/store/global';
import { TaskResponseKeyEnum, getDefaultChatVariables } from '@/constants/chat';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { useTranslation } from 'react-i18next';
import { customAlphabet } from 'nanoid';
import { userUpdateChatFeedback, adminUpdateChatFeedback } from '@/api/chat';
@@ -350,10 +350,7 @@ const ChatBox = (
messages,
controller: abortSignal,
generatingMessage,
variables: {
...getDefaultChatVariables(),
...variables
}
variables
});
// set finish status

View File

@@ -3,6 +3,7 @@ import { Menu, MenuButton, MenuItem, MenuList, MenuButtonProps } from '@chakra-u
import { getLangStore, LangEnum, setLangStore } from '@/utils/i18n';
import MyIcon from '@/components/Icon';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
const langMap = {
[LangEnum.en]: {
@@ -16,6 +17,7 @@ const langMap = {
};
const Language = (props: MenuButtonProps) => {
const router = useRouter();
const { i18n } = useTranslation();
const [language, setLanguage] = useState<`${LangEnum}`>(getLangStore());
@@ -43,6 +45,7 @@ const Language = (props: MenuButtonProps) => {
setLangStore(lang);
setLanguage(lang);
i18n?.changeLanguage?.(lang);
router.reload();
}}
>
{lang.label}

View File

@@ -21,7 +21,7 @@ interface Props extends ButtonProps {
}
const MySelect = (
{ placeholder, value, width = 'auto', list, onchange, ...props }: Props,
{ placeholder, value, width = '100%', list, onchange, ...props }: Props,
selectRef: any
) => {
const ref = useRef<HTMLButtonElement>(null);
@@ -94,6 +94,8 @@ const MySelect = (
}
zIndex={99}
transform={'translateY(35px) !important'}
maxH={'40vh'}
overflowY={'auto'}
>
{list.map((item) => (
<MenuItem

View File

@@ -67,7 +67,3 @@ export enum OutLinkTypeEnum {
export const HUMAN_ICON = `/icon/human.png`;
export const LOGO_ICON = `/icon/logo.svg`;
export const getDefaultChatVariables = () => ({
cTime: dayjs().format('YYYY/MM/DD HH:mm:ss')
});

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { Box, Flex, Button, useDisclosure, useTheme, Divider } from '@chakra-ui/react';
import React, { useCallback, useRef } from 'react';
import { Box, Flex, Button, useDisclosure, useTheme, Divider, Select } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
import { useToast } from '@/hooks/useToast';
@@ -11,6 +11,7 @@ import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { feConfigs } from '@/store/static';
import { useTranslation } from 'next-i18next';
import { timezoneList } from '@/utils/user';
import Loading from '@/components/Loading';
import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
@@ -33,6 +34,7 @@ const UserInfo = () => {
const theme = useTheme();
const { t } = useTranslation();
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
const timezones = useRef(timezoneList());
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
@@ -59,6 +61,7 @@ const UserInfo = () => {
async (data: UserType) => {
await updateUserInfo({
avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount
});
reset(data);
@@ -102,7 +105,13 @@ const UserInfo = () => {
});
return (
<Box display={['block', 'flex']} py={[2, 10]} justifyContent={'center'} fontSize={['lg', 'xl']}>
<Box
display={['block', 'flex']}
py={[2, 10]}
justifyContent={'center'}
alignItems={'flex-start'}
fontSize={['lg', 'xl']}
>
<Flex
flexDirection={'column'}
alignItems={'center'}
@@ -135,11 +144,27 @@ const UserInfo = () => {
mt={[6, 0]}
>
<Flex alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 50px'}>{t('user.Account')}:&nbsp;</Box>
<Box flex={'0 0 80px'}>{t('user.Account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 50px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={'0 0 80px'}>{t('user.Timezone')}:&nbsp;</Box>
<Select
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e.target.value });
}}
>
{timezones.current.map((item) => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={['sm', 'md']} variant={'base'} ml={5} onClick={onOpenUpdatePsw}>
{t('user.Change')}
@@ -149,7 +174,7 @@ const UserInfo = () => {
<>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'0 0 50px'}>{t('user.Balance')}:&nbsp;</Box>
<Box flex={'0 0 80px'}>{t('user.Balance')}:&nbsp;</Box>
<Box flex={1}>
<strong>{userInfo?.balance.toFixed(3)}</strong>
</Box>

View File

@@ -42,6 +42,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
/* user auth */
const { userId, user } = await authUser({ req, authBalance: true });
if (!user) {
throw new Error('user not found');
}
/* start process */
const { responseData } = await dispatchModules({
res,

View File

@@ -28,6 +28,7 @@ import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema';
import { SystemInputEnum } from '@/constants/app';
import { getSystemTime } from '@/utils/user';
export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string };
type FastGptWebChatProps = {
@@ -95,9 +96,6 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (!user) {
throw new Error('Account is error');
}
// if (authType === AuthUserTypeEnum.apikey || shareId) {
// user.openaiAccount = undefined;
// }
appId = appId ? appId : authAppid;
if (!appId) {
@@ -249,6 +247,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
});
/* running */
export async function dispatchModules({
res,
modules,
@@ -260,12 +259,16 @@ export async function dispatchModules({
}: {
res: NextApiResponse;
modules: AppModuleItemType[];
user?: UserModelSchema;
user: UserModelSchema;
params?: Record<string, any>;
variables?: Record<string, any>;
stream?: boolean;
detail?: boolean;
}) {
variables = {
...getSystemVariable({ timezone: user.timezone }),
...variables
};
const runningModules = loadModules(modules, variables);
// let storeData: Record<string, any> = {}; // after module used
@@ -390,6 +393,7 @@ export async function dispatchModules({
};
}
/* init store modules to running modules */
function loadModules(
modules: AppModuleItemType[],
variables: Record<string, any>
@@ -431,6 +435,7 @@ function loadModules(
});
}
/* sse response modules staus */
export function responseStatus({
res,
status,
@@ -451,6 +456,13 @@ export function responseStatus({
});
}
/* get system variable */
export function getSystemVariable({ timezone }: { timezone: string }) {
return {
cTime: getSystemTime(timezone)
};
}
export const config = {
api: {
bodyParser: {

View File

@@ -10,7 +10,7 @@ import { axiosConfig, getAIChatApi, openaiBaseUrl } from '@/service/lib/openai';
/* update user info */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { avatar, openaiAccount } = req.body as UserUpdateParams;
const { avatar, timezone, openaiAccount } = req.body as UserUpdateParams;
const { userId } = await authUser({ req, authToken: true });
@@ -46,6 +46,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
...(avatar && { avatar }),
...(timezone && { timezone }),
openaiAccount: openaiAccount?.key ? openaiAccount : null
}
);

View File

@@ -57,48 +57,7 @@ const AppSchema = new Schema({
default: []
},
// 弃
chat: {
relatedKbs: {
type: [Schema.Types.ObjectId],
ref: 'kb',
default: []
},
searchSimilarity: {
type: Number,
default: 0.4
},
searchLimit: {
type: Number,
default: 5
},
searchEmptyText: {
type: String,
default: ''
},
systemPrompt: {
type: String,
default: ''
},
limitPrompt: {
type: String,
default: ''
},
maxToken: {
type: Number,
default: 4000,
min: 100
},
temperature: {
type: Number,
min: 0,
max: 10,
default: 0
},
chatModel: {
// 聊天时使用的模型
type: String
}
}
chat: Object
});
try {

View File

@@ -49,6 +49,10 @@ const UserSchema = new Schema({
key: String,
baseUrl: String
}
},
timezone: {
type: String,
default: 'Asia/Shanghai'
}
});

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest } from 'next';
import Cookie from 'cookie';
import { App, OpenApi, User, OutLink, KB } from '../mongo';
import type { AppSchema } from '@/types/mongoSchema';
import type { AppSchema, UserModelSchema } from '@/types/mongoSchema';
import { ERROR_ENUM } from '../errorCode';
import { authJWT } from './tools';
@@ -25,7 +25,10 @@ export const authCookieToken = async (cookie?: string, token?: string): Promise<
/* auth balance */
export const authBalanceByUid = async (uid: string) => {
const user = await User.findById(uid);
const user = await User.findById<UserModelSchema>(
uid,
'_id username balance openaiAccount timezone'
);
if (!user) {
return Promise.reject(ERROR_ENUM.unAuthorization);
}

View File

@@ -17,6 +17,7 @@ export interface UserModelSchema {
inviterId?: string;
openaiKey: string;
createTime: number;
timezone: string;
openaiAccount?: {
key: string;
baseUrl: string;

View File

@@ -5,6 +5,7 @@ export interface UserType {
username: string;
avatar: string;
balance: number;
timezone: string;
promotionRate: UserModelSchema['promotionRate'];
openaiAccount: UserModelSchema['openaiAccount'];
}
@@ -12,6 +13,7 @@ export interface UserType {
export interface UserUpdateParams {
balance?: number;
avatar?: string;
timezone?: string;
openaiAccount?: UserModelSchema['openaiAccount'];
}

View File

@@ -1,5 +1,12 @@
import { PRICE_SCALE } from '@/constants/common';
import { loginOut } from '@/api/user';
import timezones from 'timezones-list';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const tokenKey = 'token';
export const clearToken = () => {
@@ -24,3 +31,76 @@ export const getToken = () => {
export const formatPrice = (val = 0, multiple = 1) => {
return Number(((val / PRICE_SCALE) * multiple).toFixed(10));
};
/**
* Returns the offset from UTC in hours for the current locale.
* @param {string} timeZone Timezone to get offset for
* @returns {number} The offset from UTC in hours.
*
* Generated by Trelent
*/
export const getTimezoneOffset = (timeZone: string): number => {
const now = new Date();
const tzString = now.toLocaleString('en-US', {
timeZone
});
const localString = now.toLocaleString('en-US');
const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000;
const offset = diff + now.getTimezoneOffset() / 60;
return -offset;
};
/**
* Returns a list of timezones sorted by their offset from UTC.
* @returns {object[]} A list of the given timezones sorted by their offset from UTC.
*
* Generated by Trelent
*/
export const timezoneList = () => {
const result = timezones
.map((timezone) => {
try {
let display = dayjs().tz(timezone.tzCode).format('Z');
return {
name: `(UTC${display}) ${timezone.tzCode}`,
value: timezone.tzCode,
time: getTimezoneOffset(timezone.tzCode)
};
} catch (e) {}
})
.filter((item) => item);
result.sort((a, b) => {
if (!a || !b) return 0;
if (a.time > b.time) {
return 1;
}
if (b.time > a.time) {
return -1;
}
return 0;
});
return [
{
name: 'UTC',
time: 0,
value: 'UTC'
},
...result
] as {
name: string;
value: string;
time: number;
}[];
};
export const getSystemTime = (timeZone: string) => {
const timezoneDiff = getTimezoneOffset(timeZone);
const now = Date.now();
const targetTime = now + timezoneDiff * 60 * 60 * 1000;
return dayjs(targetTime).format('YYYY-MM-DD HH:mm:ss');
};