Skip to content

Commit e3d89ac

Browse files
author
monster
committed
feat: refine task model recommendations
1 parent 73f8f57 commit e3d89ac

8 files changed

Lines changed: 113 additions & 52 deletions

File tree

frontend/src/components/console/task/model-select.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const BUILTIN_MODEL_OPTIONS = [
4141
},
4242
] as const
4343

44+
type BuiltinModelName = typeof BUILTIN_MODEL_OPTIONS[number]["model"]
45+
4446
interface ModelSelectProps {
4547
models: DomainModel[]
4648
selectedModel?: DomainModel
@@ -88,6 +90,32 @@ export default function ModelSelect({
8890
() => models.filter((model) => model.id || model.model),
8991
[models],
9092
)
93+
const recommendedModelKeys = useMemo(() => {
94+
return BUILTIN_MODEL_OPTIONS.reduce((recommended, option) => {
95+
const recommendedModel = supportedModels
96+
.filter((model) => getBuiltinModelName(model.model) === option.model)
97+
.sort((left, right) => {
98+
const weightDiff = (right.weight || 0) - (left.weight || 0)
99+
if (weightDiff !== 0) {
100+
return weightDiff
101+
}
102+
103+
const nameDiff = (left.model || "").localeCompare(right.model || "")
104+
if (nameDiff !== 0) {
105+
return nameDiff
106+
}
107+
108+
return (left.id || "").localeCompare(right.id || "")
109+
})[0]
110+
111+
const recommendedKey = recommendedModel?.id || recommendedModel?.model
112+
if (recommendedKey) {
113+
recommended[option.model] = recommendedKey
114+
}
115+
116+
return recommended
117+
}, {} as Partial<Record<BuiltinModelName, string>>)
118+
}, [supportedModels])
91119
const builtinModelGroups = useMemo(
92120
() => IS_OFFLINE_EDITION
93121
? []
@@ -197,22 +225,14 @@ export default function ModelSelect({
197225
return selectedModel ? getModelOptionDisplayName(selectedModel) : ""
198226
}
199227

200-
const getRecommendedModelBadge = (modelName?: string | null) => {
201-
const normalizedModelName = modelName?.trim().toLowerCase()
202-
if (!normalizedModelName) {
228+
const getRecommendedModelBadge = (model: DomainModel) => {
229+
const builtinModelName = getBuiltinModelName(model.model)
230+
if (!builtinModelName) {
203231
return null
204232
}
205233

206-
const builtinModelName = getBuiltinModelName(normalizedModelName)
207-
const nestedModelName = builtinModelName
208-
? normalizedModelName.slice(builtinModelName.length).replace(/^\/+/, "")
209-
: normalizedModelName
210-
211-
if (
212-
(builtinModelName === "monkeycode-basic" && nestedModelName === "qwen3.5-plus")
213-
|| (builtinModelName === "monkeycode-pro" && nestedModelName === "qwen3.6-plus")
214-
|| (builtinModelName === "monkeycode-ultra" && nestedModelName === "gpt-5.5")
215-
) {
234+
const modelKey = model.id || model.model
235+
if (modelKey && recommendedModelKeys[builtinModelName] === modelKey) {
216236
return t("taskWorkflow.model.recommended")
217237
}
218238

@@ -221,7 +241,7 @@ export default function ModelSelect({
221241

222242
const renderModelOption = (model: DomainModel, nested = false, indented = false) => {
223243
const displayName = getModelOptionDisplayName(model, nested)
224-
const recommendedBadge = getRecommendedModelBadge(model.model)
244+
const recommendedBadge = getRecommendedModelBadge(model)
225245

226246
return (
227247
<DropdownMenuRadioItem

frontend/src/i18n/resources/cn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1440,7 +1440,7 @@ const cn = {
14401440
action: "去 GitHub 反馈",
14411441
},
14421442
quota: {
1443-
freeQuota: "免费额度",
1443+
freeQuota: "会员额度",
14441444
freeWithValue: "今日额度 {{amount}}",
14451445
remainingToday: "今日剩余 {{amount}}",
14461446
noQuota: "无额度",

frontend/src/i18n/resources/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1440,7 +1440,7 @@ const en = {
14401440
action: "Give feedback on GitHub",
14411441
},
14421442
quota: {
1443-
freeQuota: "Free quota",
1443+
freeQuota: "Membership quota",
14441444
freeWithValue: "Daily quota {{amount}}",
14451445
remainingToday: "{{amount}} left today",
14461446
noQuota: "No quota",

frontend/src/pages/console/user/task/task-detail.tsx

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ConstsOwnerType, ConstsTaskStatus, type DomainModel, type DomainProjectTask, type DomainVMPort } from "@/api/Api"
2+
import { useAppRuntime } from "@/components/app-runtime-provider"
23
import { useBreadcrumbTask } from "@/components/console/breadcrumb-task-context"
34
import { useCommonData } from "@/components/console/data-provider"
45
import { PlanStepsBlock } from "@/components/console/task/chat-panel"
@@ -62,13 +63,15 @@ const BUILTIN_TASK_MODEL_OPTIONS = [
6263
{ model: "monkeycode-pro", labelKey: "pro", badgeKey: "proBadge", badgeVariant: "secondary" as const, iconName: "vip-1" },
6364
{ model: "monkeycode-ultra", labelKey: "ultra", badgeKey: "ultraBadge", badgeVariant: "secondary" as const, iconName: "vip-2" },
6465
] as const
66+
type BuiltinTaskModelName = typeof BUILTIN_TASK_MODEL_OPTIONS[number]["model"]
6567
const OPEN_WALLET_DIALOG_EVENT = "open-wallet-dialog"
6668
type MessageSource = "live" | "history"
6769
const MODEL_SWITCH_MIN_CREATED_AT = 1777381200 // 2026-04-28 21:00:00 +08:00
6870

6971
export default function TaskDetailPage() {
7072
const { taskId } = useParams()
7173
const { setTaskName } = useBreadcrumbTask() ?? {}
74+
const { serverConfig } = useAppRuntime()
7275
const { models, loadingModels, subscription } = useCommonData()
7376
const { t } = useTranslation()
7477
const isMobile = useIsMobile()
@@ -133,6 +136,7 @@ export default function TaskDetailPage() {
133136
const previewDialogOpenRef = React.useRef(false)
134137
const showPreparing = useShouldShowPreparing(task)
135138
const taskInteractive = task?.status === ConstsTaskStatus.TaskStatusProcessing
139+
const canPublishWebsite = !IS_OFFLINE_EDITION && serverConfig?.region === "cn"
136140
const envid = task?.virtualmachine?.id
137141
const cancelledRef = React.useRef(false)
138142
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -274,6 +278,32 @@ export default function TaskDetailPage() {
274278
() => models.filter((model) => model.id || model.model),
275279
[models]
276280
)
281+
const recommendedModelKeys = React.useMemo(() => {
282+
return BUILTIN_TASK_MODEL_OPTIONS.reduce((recommended, option) => {
283+
const recommendedModel = supportedModels
284+
.filter((model) => getBuiltinModelName(model.model) === option.model)
285+
.sort((left, right) => {
286+
const weightDiff = (right.weight || 0) - (left.weight || 0)
287+
if (weightDiff !== 0) {
288+
return weightDiff
289+
}
290+
291+
const nameDiff = (left.model || "").localeCompare(right.model || "")
292+
if (nameDiff !== 0) {
293+
return nameDiff
294+
}
295+
296+
return (left.id || "").localeCompare(right.id || "")
297+
})[0]
298+
299+
const recommendedKey = recommendedModel?.id || recommendedModel?.model
300+
if (recommendedKey) {
301+
recommended[option.model] = recommendedKey
302+
}
303+
304+
return recommended
305+
}, {} as Partial<Record<BuiltinTaskModelName, string>>)
306+
}, [supportedModels])
277307
const modelGroups = React.useMemo(() => {
278308
const builtinModelGroups = IS_OFFLINE_EDITION
279309
? []
@@ -774,34 +804,26 @@ export default function TaskDetailPage() {
774804
return currentModel ? getModelOptionDisplayName(currentModel) : getModelDisplayName(currentModelName)
775805
}, [builtinTaskModelOptions, currentModel, currentModelName, getModelOptionDisplayName, isMobile])
776806

777-
const getRecommendedModelBadge = React.useCallback((modelName?: string | null) => {
778-
const normalizedModelName = modelName?.trim().toLowerCase()
779-
if (!normalizedModelName) {
807+
const getRecommendedModelBadge = React.useCallback((model: DomainModel) => {
808+
const builtinModelName = getBuiltinModelName(model.model)
809+
if (!builtinModelName) {
780810
return null
781811
}
782812

783-
const builtinModelName = getBuiltinModelName(normalizedModelName)
784-
const nestedModelName = builtinModelName
785-
? normalizedModelName.slice(builtinModelName.length).replace(/^\/+/, "")
786-
: normalizedModelName
787-
788-
if (
789-
(builtinModelName === "monkeycode-basic" && nestedModelName === "qwen3.5-plus")
790-
|| (builtinModelName === "monkeycode-pro" && nestedModelName === "qwen3.6-plus")
791-
|| (builtinModelName === "monkeycode-ultra" && nestedModelName === "gpt-5.5")
792-
) {
813+
const modelKey = model.id || model.model
814+
if (modelKey && recommendedModelKeys[builtinModelName] === modelKey) {
793815
return t("taskDetail.page.models.recommended")
794816
}
795817

796818
return null
797-
}, [t])
819+
}, [recommendedModelKeys, t])
798820

799821
const renderModelSwitchOption = React.useCallback((model: DomainModel, nested = false, indented = false) => {
800822
const modelName = model.model || t("taskDetail.page.models.unknown")
801823
const isSelected = model.id === currentModelId || (!currentModelId && model.model === currentModelName)
802824
const canUseModel = canUseModelBySubscription(model, subscription)
803825
const displayName = getModelOptionDisplayName(model, nested)
804-
const recommendedBadge = getRecommendedModelBadge(model.model)
826+
const recommendedBadge = getRecommendedModelBadge(model)
805827

806828
return (
807829
<DropdownMenuRadioItem
@@ -1294,7 +1316,7 @@ export default function TaskDetailPage() {
12941316
<IconDeviceDesktop className="size-3.5" />
12951317
{t("taskDetail.panels.preview")}{previewPortCount > 0 ? ` (${previewPortCount})` : ""}
12961318
</Button>
1297-
{!IS_OFFLINE_EDITION && (
1319+
{canPublishWebsite && (
12981320
<Button
12991321
variant="ghost"
13001322
size="sm"
@@ -1530,24 +1552,26 @@ export default function TaskDetailPage() {
15301552
/>
15311553
</DialogContent>
15321554
</Dialog>
1533-
<Dialog open={publishConfirmDialogOpen} onOpenChange={setPublishConfirmDialogOpen}>
1534-
<DialogContent>
1535-
<DialogHeader>
1536-
<DialogTitle>{t("taskDetail.page.dialogs.publishWebsite.title")}</DialogTitle>
1537-
<DialogDescription>
1538-
{t("taskDetail.page.dialogs.publishWebsite.description")}
1539-
</DialogDescription>
1540-
</DialogHeader>
1541-
<DialogFooter>
1542-
<Button variant="outline" onClick={() => setPublishConfirmDialogOpen(false)}>
1543-
{t("taskDetail.common.cancel")}
1544-
</Button>
1545-
<Button onClick={handleConfirmPublishWebsite}>
1546-
{t("taskDetail.page.dialogs.publishWebsite.confirm")}
1547-
</Button>
1548-
</DialogFooter>
1549-
</DialogContent>
1550-
</Dialog>
1555+
{canPublishWebsite && (
1556+
<Dialog open={publishConfirmDialogOpen} onOpenChange={setPublishConfirmDialogOpen}>
1557+
<DialogContent>
1558+
<DialogHeader>
1559+
<DialogTitle>{t("taskDetail.page.dialogs.publishWebsite.title")}</DialogTitle>
1560+
<DialogDescription>
1561+
{t("taskDetail.page.dialogs.publishWebsite.description")}
1562+
</DialogDescription>
1563+
</DialogHeader>
1564+
<DialogFooter>
1565+
<Button variant="outline" onClick={() => setPublishConfirmDialogOpen(false)}>
1566+
{t("taskDetail.common.cancel")}
1567+
</Button>
1568+
<Button onClick={handleConfirmPublishWebsite}>
1569+
{t("taskDetail.page.dialogs.publishWebsite.confirm")}
1570+
</Button>
1571+
</DialogFooter>
1572+
</DialogContent>
1573+
</Dialog>
1574+
)}
15511575
</div>
15521576
)
15531577
}

frontend/test/console-nav-rewards-i18n.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ test("侧边栏在线权益入口提供中英文资源", () => {
4848
assert.equal(en.consoleShell.rewards.checkin.label, "Check in for credits");
4949
assert.equal(cn.consoleShell.rewards.invite.title, "邀请注册赚积分");
5050
assert.equal(en.consoleShell.rewards.invite.title, "Invite users for credits");
51-
assert.equal(cn.consoleShell.rewards.quota.freeQuota, "免费额度");
52-
assert.equal(en.consoleShell.rewards.quota.freeQuota, "Free quota");
51+
assert.equal(cn.consoleShell.rewards.quota.freeQuota, "会员额度");
52+
assert.equal(en.consoleShell.rewards.quota.freeQuota, "Membership quota");
5353
assert.equal(cn.consoleShell.rewards.quota.freeWithValue, "今日额度 {{amount}}");
5454
assert.equal(en.consoleShell.rewards.quota.freeWithValue, "Daily quota {{amount}}");
5555
assert.equal(cn.consoleShell.rewards.quota.creditsSummary, "积分 {{value}}");

frontend/test/task-detail-page-i18n.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@ test("任务详情页面壳提供中英文资源", () => {
2828
assert.equal(cn.taskDetail.page.dialogs.switchModel.title, "切换模型");
2929
assert.equal(en.taskDetail.page.dialogs.switchModel.title, "Switch model");
3030
});
31+
32+
test("任务详情模型切换推荐标按同级别模型 weight 计算", () => {
33+
assert.match(source, /recommendedModelKeys/);
34+
assert.match(source, /getBuiltinModelName\(model\.model\) === option\.model/);
35+
assert.match(source, /\(right\.weight \|\| 0\) - \(left\.weight \|\| 0\)/);
36+
assert.doesNotMatch(source, /qwen3\.5-plus|qwen3\.6-plus|gpt-5\.5/);
37+
});

frontend/test/task-detail-preview-publish.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ test("任务详情预览弹窗支持确认后自动发送发布指令", () => {
2626
assert.doesNotMatch(chatInputSource, /taskDetail\.chat\.commands\.publishWebsiteDescription/);
2727

2828
assert.match(pageSource, /TaskChatInputBoxHandle/);
29+
assert.match(pageSource, /useAppRuntime/);
30+
assert.match(pageSource, /const canPublishWebsite = !IS_OFFLINE_EDITION && serverConfig\?\.region === "cn"/);
2931
assert.match(pageSource, /chatInputRef/);
3032
assert.match(pageSource, /publishConfirmDialogOpen/);
3133
assert.match(pageSource, /chatInputRef\.current\?\.submitPublishWebsite\(\)/);
@@ -39,6 +41,7 @@ test("任务详情预览弹窗支持确认后自动发送发布指令", () => {
3941
/<IconTerminal2[\s\S]*?t\("taskDetail\.panels\.preview"\)[\s\S]*?<IconUpload[\s\S]*?t\("taskDetail\.page\.dialogs\.publishWebsite\.button"\)[\s\S]*?<\/div>/,
4042
);
4143
assert.ok(panelButtonGroupMatch, "publish button should sit next to terminal, files, and preview controls");
44+
assert.match(panelButtonGroupMatch[0], /canPublishWebsite/);
4245
assert.match(panelButtonGroupMatch[0], /disabled=\{!canInput\}/);
4346

4447
const previewDialogMatch = pageSource.match(

frontend/test/task-workflow-i18n.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,10 @@ test("任务工作流共享组件提供中英文资源", () => {
6868
assert.equal(cn.taskWorkflow.actions.delete, "删除任务");
6969
assert.equal(en.taskWorkflow.actions.delete, "Delete");
7070
});
71+
72+
test("模型选择框推荐标按同级别模型 weight 计算", () => {
73+
assert.match(sourceFiles.modelSelect, /recommendedModelKeys/);
74+
assert.match(sourceFiles.modelSelect, /getBuiltinModelName\(model\.model\) === option\.model/);
75+
assert.match(sourceFiles.modelSelect, /\(right\.weight \|\| 0\) - \(left\.weight \|\| 0\)/);
76+
assert.doesNotMatch(sourceFiles.modelSelect, /qwen3\.5-plus|qwen3\.6-plus|gpt-5\.5/);
77+
});

0 commit comments

Comments
 (0)