Skip to content

Commit

Permalink
feat: support custom subscription config (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
zmh-program committed Jan 18, 2024
1 parent aacc6b9 commit ed74b6b
Show file tree
Hide file tree
Showing 27 changed files with 1,198 additions and 107 deletions.
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"package": {
"productName": "chatnio",
"version": "3.8.6"
"version": "3.9.0"
},
"tauri": {
"allowlist": {
Expand Down
36 changes: 36 additions & 0 deletions app/src/admin/api/plan.ts
Original file line number Diff line number Diff line change
@@ -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<PlanConfig> {
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<CommonResponse> {
try {
const response = await axios.post(`/admin/plan/update`, config);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
2 changes: 1 addition & 1 deletion app/src/api/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
124 changes: 124 additions & 0 deletions app/src/assets/admin/subscription.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
94 changes: 94 additions & 0 deletions app/src/components/ui/multi-combobox.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-[320px] max-w-[60vw] justify-between", className)}
>
<Check className="mr-2 h-4 w-4 shrink-0 opacity-50" />
{placeholder ?? `${value.length} Items Selected`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[320px] max-w-[60vw] p-0" align={align}>
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandEmpty>{t("admin.empty")}</CommandEmpty>
<CommandList>
{valueList.map((key) => (
<CommandItem
key={key}
value={key}
onSelect={(current) => {
if (value.includes(current)) {
onChange(value.filter((item) => item !== current));
} else {
onChange([...value, current]);
}
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(key) ? "opacity-100" : "opacity-0",
)}
/>
{key}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
4 changes: 2 additions & 2 deletions app/src/components/ui/number-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
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);
Expand Down
Loading

0 comments on commit ed74b6b

Please sign in to comment.