diff --git a/app/package.json b/app/package.json index 4239d60c..4ff083dd 100644 --- a/app/package.json +++ b/app/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toggle": "^1.0.3", + "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@reduxjs/toolkit": "^1.9.5", "@tanem/react-nprogress": "^5.0.51", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 666ce2e0..86667a12 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: '@radix-ui/react-toggle': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle-group': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.0.6 version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) @@ -1588,6 +1591,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0) + '@types/react': 18.2.33 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==} peerDependencies: diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index 3ae5b59e..7debea7a 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "chatnio", - "version": "3.8.6" + "version": "3.9.0" }, "tauri": { "allowlist": { diff --git a/app/src/admin/api/plan.ts b/app/src/admin/api/plan.ts new file mode 100644 index 00000000..433f0286 --- /dev/null +++ b/app/src/admin/api/plan.ts @@ -0,0 +1,36 @@ +import { Plan } from "@/api/types"; +import axios from "axios"; +import { CommonResponse } from "@/admin/utils.ts"; +import { getErrorMessage } from "@/utils/base.ts"; + +export type PlanConfig = { + enabled: boolean; + plans: Plan[]; +}; + +export async function getPlanConfig(): Promise { + try { + const response = await axios.get("/admin/plan/view"); + const conf = response.data as PlanConfig; + conf.plans = (conf.plans || []).filter((item) => item.level > 0); + if (conf.plans.length === 0) + conf.plans = [1, 2, 3].map( + (level) => ({ level, price: 0, items: [] }) as Plan, + ); + return conf; + } catch (e) { + console.warn(e); + return { enabled: false, plans: [] }; + } +} + +export async function setPlanConfig( + config: PlanConfig, +): Promise { + try { + const response = await axios.post(`/admin/plan/update`, config); + return response.data as CommonResponse; + } catch (e) { + return { status: false, error: getErrorMessage(e) }; + } +} diff --git a/app/src/api/connection.ts b/app/src/api/connection.ts index 2d10e4c8..3a3e4d59 100644 --- a/app/src/api/connection.ts +++ b/app/src/api/connection.ts @@ -3,7 +3,7 @@ import { getMemory } from "@/utils/memory.ts"; import { getErrorMessage } from "@/utils/base.ts"; export const endpoint = `${websocketEndpoint}/chat`; -export const maxRetry = 5; +export const maxRetry = 30; // 15s max websocket retry export type StreamMessage = { conversation?: number; diff --git a/app/src/assets/admin/subscription.less b/app/src/assets/admin/subscription.less index 3ef4840e..e7ab79bc 100644 --- a/app/src/assets/admin/subscription.less +++ b/app/src/assets/admin/subscription.less @@ -11,3 +11,127 @@ min-height: 20vh; } } + +.plan-config { + display: flex; + flex-direction: column; + margin-top: 0.25rem; + + & > * { + margin-bottom: 1rem; + + &:last-child { + margin-bottom: 0; + } + } + + .plan-config-row { + display: flex; + flex-direction: row; + align-items: center; + } + + .plan-config-card { + display: flex; + flex-direction: column; + padding: 1rem; + border-radius: var(--radius); + border: 1px solid hsl(var(--border)); + + .plan-config-title { + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + user-select: none; + margin-bottom: 0.75rem; + + &:before { + display: inline-block; + content: ''; + margin-right: 0.5rem; + height: 1.25rem; + width: 2px; + border-radius: 1px; + background: hsl(var(--text-secondary)); + transition: .25s; + } + } + + .plan-items-action { + display: flex; + flex-direction: row; + gap: 1rem; + align-items: center; + flex-wrap: wrap; + margin-top: 1rem; + } + + .plan-items-wrapper { + display: flex; + flex-direction: column; + width: 100%; + height: max-content; + margin-top: 1rem; + + .plan-item { + padding: 1rem; + border-radius: var(--radius); + border: 1px solid hsl(var(--border)); + display: flex; + flex-direction: column; + + .plan-editor-row > p { + min-width: 4.25rem; + } + + & > * { + margin-bottom: 1rem; + + &:last-child { + margin-bottom: 0; + } + } + } + + & > * { + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + } + } + + .plan-editor-row { + display: flex; + flex-direction: row; + align-items: center; + + .plan-editor-label { + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + margin-right: 0.5rem; + + svg { + display: inline-block; + flex-shrink: 0; + } + } + + & > p { + white-space: nowrap; + } + } + + & > * { + margin-bottom: 0.25rem; + + &:last-child { + margin-bottom: 0; + } + } + } +} diff --git a/app/src/components/ui/multi-combobox.tsx b/app/src/components/ui/multi-combobox.tsx new file mode 100644 index 00000000..73d908c1 --- /dev/null +++ b/app/src/components/ui/multi-combobox.tsx @@ -0,0 +1,94 @@ +import React from "react"; + +import { cn } from "@/components/ui/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +type MultiComboBoxProps = { + value: string[]; + onChange: (value: string[]) => void; + list: string[]; + placeholder?: string; + searchPlaceholder?: string; + defaultOpen?: boolean; + className?: string; + align?: "start" | "end" | "center" | undefined; +}; + +export function MultiCombobox({ + value, + onChange, + list, + placeholder, + searchPlaceholder, + defaultOpen, + className, + align, +}: MultiComboBoxProps) { + const { t } = useTranslation(); + const [open, setOpen] = React.useState(defaultOpen ?? false); + const valueList = React.useMemo((): string[] => { + // list set + const set = new Set(list); + return [...set]; + }, [list]); + + return ( + + + + + + + + {t("admin.empty")} + + {valueList.map((key) => ( + { + if (value.includes(current)) { + onChange(value.filter((item) => item !== current)); + } else { + onChange([...value, current]); + } + }} + > + + {key} + + ))} + + + + + ); +} diff --git a/app/src/components/ui/number-input.tsx b/app/src/components/ui/number-input.tsx index 25067aeb..b1219f57 100644 --- a/app/src/components/ui/number-input.tsx +++ b/app/src/components/ui/number-input.tsx @@ -37,8 +37,8 @@ const NumberInput = React.forwardRef( return v.match(exp)?.join("") || ""; } - // replace -0124.5 to -124.5, 0043 to 43 - const exp = /^[-+]?0+(?=[1-9])|(?<=\.)0+(?=[1-9])|(?<=\.)0+$/g; + // replace -0124.5 to -124.5, 0043 to 43, 2.000 to 2.000 + const exp = /^[-+]?0+(?=[0-9]+(\.[0-9]+)?$)/; v = v.replace(exp, ""); const raw = getNumber(v, props.acceptNegative); diff --git a/app/src/components/ui/toggle-group.tsx b/app/src/components/ui/toggle-group.tsx new file mode 100644 index 00000000..c97bc942 --- /dev/null +++ b/app/src/components/ui/toggle-group.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; +import { VariantProps } from "class-variance-authority"; + +import { cn } from "@/components/ui/lib/utils"; +import { toggleVariants } from "@/components/ui/toggle"; + +const ToggleGroupContext = React.createContext< + VariantProps +>({ + size: "default", + variant: "default", +}); + +const ToggleGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, children, ...props }, ref) => ( + + + {children} + + +)); + +ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; + +const ToggleGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, children, variant, size, ...props }, ref) => { + const context = React.useContext(ToggleGroupContext); + + return ( + + {children} + + ); +}); + +ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; + +export { ToggleGroup, ToggleGroupItem }; diff --git a/app/src/conf/index.ts b/app/src/conf/index.ts index 2fc333e1..7936c930 100644 --- a/app/src/conf/index.ts +++ b/app/src/conf/index.ts @@ -9,7 +9,7 @@ import { syncSiteInfo } from "@/admin/api/info.ts"; import { getOfflineModels, loadPreferenceModels } from "@/conf/storage.ts"; import { setAxiosConfig } from "@/conf/api.ts"; -export const version = "3.8.6"; // version of the current build +export const version = "3.9.0"; // version of the current build export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin) export const deploy: boolean = true; // is production environment (for api endpoint) export const tokenField = getTokenField(deploy); // token field name for storing token diff --git a/app/src/conf/subscription.tsx b/app/src/conf/subscription.tsx index 2ceb72b6..93c98efe 100644 --- a/app/src/conf/subscription.tsx +++ b/app/src/conf/subscription.tsx @@ -5,6 +5,9 @@ import { ImagePlus, Video, AudioLines, + Container, + Archive, + Flame, } from "lucide-react"; import React, { useMemo } from "react"; import { Plan, Plans } from "@/api/types.ts"; @@ -17,8 +20,13 @@ export const subscriptionIcons: Record = { booktext: , video: