Skip to content

Commit 79486b3

Browse files
committed
Add model pricing shortcuts and stabilize vm setup
1 parent 1329352 commit 79486b3

File tree

5 files changed

+320
-124
lines changed

5 files changed

+320
-124
lines changed

frontend/src/components/console/nav/nav-balance.tsx

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
2-
import { IconCoin, IconCrown, IconGift, IconLockCode, IconLogout, IconMail, IconUserHexagon, IconCpu, IconPencil, IconCamera } from "@tabler/icons-react";
2+
import { IconChevronDown, IconCoin, IconCrown, IconGift, IconLockCode, IconLogout, IconMail, IconUserHexagon, IconCpu, IconPencil, IconCamera } from "@tabler/icons-react";
33
import { useEffect, useState, useRef, useCallback, type ChangeEvent } from "react";
44
import { apiRequest } from "@/utils/requestUtils";
55
import { Input } from "@/components/ui/input";
@@ -11,6 +11,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1111
import { Badge } from "@/components/ui/badge";
1212
import { Api, ConstsTransactionKind, ConstsUploadUsage, type DomainInvitationItem, type DomainTransactionLog } from "@/api/Api";
1313
import { Item, ItemContent, ItemGroup, ItemSeparator, ItemTitle } from "@/components/ui/item";
14+
import Icon from "@/components/common/Icon";
1415
import dayjs from "dayjs";
1516
import { cn } from "@/lib/utils";
1617
import {
@@ -24,6 +25,12 @@ import {
2425
AlertDialogTitle,
2526
} from "@/components/ui/alert-dialog";
2627
import { Label } from "@/components/ui/label";
28+
import {
29+
DropdownMenu,
30+
DropdownMenuContent,
31+
DropdownMenuItem,
32+
DropdownMenuTrigger,
33+
} from "@/components/ui/dropdown-menu";
2734
import {
2835
Sidebar,
2936
SidebarContent,
@@ -36,7 +43,7 @@ import {
3643
SidebarProvider,
3744
} from "@/components/ui/sidebar";
3845
import { useCommonData } from "../data-provider";
39-
import { captchaChallenge, getSubscriptionPlanLabel, getSubscriptionPlanShortLabel, hasProSubscription, isValidEmail } from "@/utils/common";
46+
import { captchaChallenge, getBrandFromModelName, getSubscriptionPlanLabel, getSubscriptionPlanShortLabel, hasProSubscription, isValidEmail } from "@/utils/common";
4047
import { useNavigate } from "react-router-dom";
4148

4249
interface NavBalanceProps {
@@ -56,19 +63,22 @@ const BALANCE_NAV = [
5663
] as const
5764

5865
const MODEL_PRICING = [
59-
{ model: "minimax-m2.7", credits: 0 },
60-
{ model: "gpt5.4", credits: 1000 },
61-
{ model: "gpt5.2", credits: 600 },
62-
{ model: "gpt5.3-codex", credits: 600 },
63-
{ model: "gpt5.1", credits: 500 },
64-
{ model: "glm-5.1", credits: 600 },
65-
{ model: "glm-5", credits: 400 },
66-
{ model: "glm-4.7", credits: 200 },
67-
{ model: "qwen3-max", credits: 400 },
68-
{ model: "qwen3.6-plus", credits: 200 },
69-
{ model: "kimi-k2.5", credits: 400 },
66+
{ model: "minimax-m2.7", credits: 0, score: 637 },
67+
{ model: "qwen3.5-plus", credits: 0, score: 538 },
68+
{ model: "gpt5.4", credits: 1000, score: 922 },
69+
{ model: "gpt5.2", credits: 600, score: 887 },
70+
{ model: "gpt5.3-codex", credits: 600, score: 918 },
71+
{ model: "gpt5.1", credits: 500, score: 883 },
72+
{ model: "glm-5.1", credits: 600, score: 904 },
73+
{ model: "glm-5", credits: 400, score: 847 },
74+
{ model: "glm-4.7", credits: 200, score: 709 },
75+
{ model: "qwen3-max", credits: 400, score: 840 },
76+
{ model: "qwen3.6-plus", credits: 200, score: 751 },
77+
{ model: "kimi-k2.5", credits: 400, score: 889 },
7078
] as const
7179

80+
const TOP_MODEL_COUNT = 3
81+
7282
type BalanceSectionId = (typeof BALANCE_NAV)[number]["id"]
7383
type WalletSectionId = BalanceSectionId | "profile" | "plan" | "balance"
7484

@@ -105,6 +115,7 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
105115
const [newName, setNewName] = useState("");
106116
const [changingName, setChangingName] = useState(false);
107117
const [uploadingAvatar, setUploadingAvatar] = useState(false);
118+
const [pricingSort, setPricingSort] = useState<"value" | "score" | "name">("value")
108119
const loadMoreRef = useRef<HTMLDivElement>(null);
109120
const contentScrollRef = useRef<HTMLDivElement>(null);
110121
const avatarInputRef = useRef<HTMLInputElement>(null);
@@ -168,6 +179,35 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
168179
return normalized.toString()
169180
}
170181
const getInvitationInitial = (name?: string) => name?.trim().charAt(0).toUpperCase() || "?"
182+
const pricingSortLabel = pricingSort === "value"
183+
? "性价比排序"
184+
: pricingSort === "score"
185+
? "代码能力排序"
186+
: "按名字排序"
187+
const topScoreModels = [...MODEL_PRICING]
188+
.sort((a, b) => b.score - a.score)
189+
.slice(0, TOP_MODEL_COUNT)
190+
const scoreRankMap = new Map(topScoreModels.map((item, index) => [item.model, index + 1]))
191+
const sortedModelPricing = [...MODEL_PRICING].sort((a, b) => {
192+
if (pricingSort === "name") {
193+
return a.model.localeCompare(b.model)
194+
}
195+
196+
if (pricingSort === "score") {
197+
return b.score - a.score
198+
}
199+
200+
if (a.credits === 0 && b.credits === 0) {
201+
return b.score - a.score
202+
}
203+
if (a.credits === 0) {
204+
return -1
205+
}
206+
if (b.credits === 0) {
207+
return 1
208+
}
209+
return (b.score / b.credits) - (a.score / a.credits)
210+
})
171211

172212
const formatSubscriptionExpiry = (expiresAt?: string) => {
173213
if (!expiresAt) {
@@ -1133,32 +1173,69 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
11331173

11341174
const pricingContent = (
11351175
<div className="space-y-4">
1136-
<div className="rounded-md border p-5">
1137-
<div className="text-md font-medium">计费说明</div>
1138-
<div className="mt-2 text-sm text-muted-foreground">
1139-
以下价格单位为每百万 token 消耗多少积分。免费模型不会扣减积分。
1176+
<div className="flex items-center justify-between gap-4">
1177+
<div className="text-sm text-muted-foreground">
1178+
价格按每百万 Token 计算
11401179
</div>
1180+
<DropdownMenu>
1181+
<DropdownMenuTrigger asChild>
1182+
<Button type="button" size="sm" variant="outline" className="gap-2">
1183+
<span>{pricingSortLabel}</span>
1184+
<IconChevronDown className="size-4" />
1185+
</Button>
1186+
</DropdownMenuTrigger>
1187+
<DropdownMenuContent align="end">
1188+
<DropdownMenuItem onClick={() => setPricingSort("value")}>
1189+
性价比排序
1190+
</DropdownMenuItem>
1191+
<DropdownMenuItem onClick={() => setPricingSort("score")}>
1192+
代码能力排序
1193+
</DropdownMenuItem>
1194+
<DropdownMenuItem onClick={() => setPricingSort("name")}>
1195+
按名字排序
1196+
</DropdownMenuItem>
1197+
</DropdownMenuContent>
1198+
</DropdownMenu>
11411199
</div>
1142-
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
1143-
{MODEL_PRICING.map((item) => (
1144-
<div key={item.model} className="rounded-md border p-4">
1145-
<div className="flex items-start justify-between gap-3">
1146-
<div className="min-w-0">
1147-
<div className="truncate font-mono text-sm font-medium">{item.model}</div>
1148-
<div className="mt-1 text-xs text-muted-foreground">每百万 token</div>
1149-
</div>
1150-
<div
1151-
className={cn(
1152-
"shrink-0 rounded-full px-2.5 py-1 text-xs font-medium",
1153-
item.credits === 0 ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary",
1154-
)}
1155-
>
1156-
{item.credits === 0 ? "免费" : `${formatPoints(item.credits)} 积分`}
1157-
</div>
1158-
</div>
1200+
<ItemGroup className="flex flex-col gap-0 has-data-[size=sm]:gap-0 has-data-[size=xs]:gap-0">
1201+
{sortedModelPricing.map((item, index) => (
1202+
<div key={item.model}>
1203+
<Item variant="default" size="sm" className="px-2 py-3">
1204+
<ItemContent>
1205+
<ItemTitle className="grid w-full grid-cols-[minmax(0,1.6fr)_minmax(120px,0.8fr)_minmax(100px,0.6fr)] items-center gap-4">
1206+
<div className="flex min-w-0 items-center gap-2">
1207+
<Icon name={getBrandFromModelName(item.model)} className="size-4 shrink-0" />
1208+
<div className="truncate font-mono text-sm font-medium">{item.model}</div>
1209+
</div>
1210+
<div>
1211+
<Badge
1212+
variant={item.credits === 0 ? "default" : "outline"}
1213+
className={cn(
1214+
"h-6 rounded-full px-2.5 text-xs font-medium",
1215+
item.credits === 0 && "hover:bg-primary",
1216+
)}
1217+
>
1218+
{item.credits === 0 ? "免费" : `${formatPoints(item.credits)} 积分 / 1M`}
1219+
</Badge>
1220+
</div>
1221+
<div className="flex justify-end">
1222+
<Badge
1223+
variant={scoreRankMap.has(item.model) ? "default" : "secondary"}
1224+
className={cn(
1225+
"h-6 rounded-full px-2.5 text-xs font-medium tabular-nums",
1226+
scoreRankMap.has(item.model) && "bg-amber-500 text-white hover:bg-amber-500",
1227+
)}
1228+
>
1229+
{item.score}
1230+
</Badge>
1231+
</div>
1232+
</ItemTitle>
1233+
</ItemContent>
1234+
</Item>
1235+
{index < sortedModelPricing.length - 1 && <ItemSeparator className="my-0" />}
11591236
</div>
11601237
))}
1161-
</div>
1238+
</ItemGroup>
11621239
</div>
11631240
)
11641241

frontend/src/components/console/project/start-develop-task-dialog.tsx

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
1111
import { Spinner } from "@/components/ui/spinner"
1212
import { canUseModelBySubscription, getBrandFromModelName, getOwnerTypeBadge, selectHost, selectImage, selectPreferredTaskModel } from "@/utils/common"
1313
import { apiRequest } from "@/utils/requestUtils"
14-
import { IconSparkles } from "@tabler/icons-react"
14+
import { IconHelpCircle, IconSparkles } from "@tabler/icons-react"
1515
import { useState, useEffect, useRef } from "react"
1616
import { useNavigate } from "react-router-dom"
1717
import { toast } from "sonner"
@@ -24,6 +24,8 @@ interface StartDevelopTaskDialogProps {
2424
project?: DomainProject
2525
}
2626

27+
const OPEN_WALLET_DIALOG_EVENT = "open-wallet-dialog"
28+
2729
export default function StartDevelopTaskDialog({
2830
open,
2931
onOpenChange,
@@ -40,6 +42,12 @@ export default function StartDevelopTaskDialog({
4042
const [selectedModelId, setSelectedModelId] = useState<string>('')
4143
const { images, models, hosts, subscription } = useCommonData()
4244

45+
const handleOpenModelPricing = () => {
46+
window.dispatchEvent(new CustomEvent(OPEN_WALLET_DIALOG_EVENT, {
47+
detail: { section: "pricing" },
48+
}))
49+
}
50+
4351
const fetchBranches = async () => {
4452
if (!project?.git_identity_id || !project?.repo_url) {
4553
return
@@ -242,23 +250,35 @@ export default function StartDevelopTaskDialog({
242250
)}
243251
<div className="space-y-2">
244252
<Label>大模型</Label>
245-
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
246-
<SelectTrigger className="w-full">
247-
<SelectValue placeholder="选择大模型" />
248-
</SelectTrigger>
249-
<SelectContent>
250-
{models.map((model) => (
251-
<SelectItem key={model.id} value={model.id || ""}>
252-
<Icon name={getBrandFromModelName(model.model || '')} className="size-4" />
253-
{model.model}
254-
{model.owner?.type !== ConstsOwnerType.OwnerTypePublic && getOwnerTypeBadge(model.owner)}
255-
{model.owner?.type === ConstsOwnerType.OwnerTypePublic && model.is_free === true && (
256-
<Badge className="!text-primary-foreground">免费</Badge>
257-
)}
258-
</SelectItem>
259-
))}
260-
</SelectContent>
261-
</Select>
253+
<div className="flex items-center gap-2">
254+
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
255+
<SelectTrigger className="w-full">
256+
<SelectValue placeholder="选择大模型" />
257+
</SelectTrigger>
258+
<SelectContent>
259+
{models.map((model) => (
260+
<SelectItem key={model.id} value={model.id || ""}>
261+
<Icon name={getBrandFromModelName(model.model || '')} className="size-4" />
262+
{model.model}
263+
{model.owner?.type !== ConstsOwnerType.OwnerTypePublic && getOwnerTypeBadge(model.owner)}
264+
{model.owner?.type === ConstsOwnerType.OwnerTypePublic && model.is_free === true && (
265+
<Badge className="!text-primary-foreground">免费</Badge>
266+
)}
267+
</SelectItem>
268+
))}
269+
</SelectContent>
270+
</Select>
271+
<Button
272+
type="button"
273+
size="icon-sm"
274+
variant="outline"
275+
className="shrink-0"
276+
onClick={handleOpenModelPricing}
277+
aria-label="查看模型定价"
278+
>
279+
<IconHelpCircle className="size-4" />
280+
</Button>
281+
</div>
262282
</div>
263283
<div className="space-y-2">
264284
<Label>任务内容</Label>

0 commit comments

Comments
 (0)