Skip to content

Commit 5355cc4

Browse files
author
monster
committed
fix(frontend): support task resources in offline mode
1 parent c58bcd4 commit 5355cc4

8 files changed

Lines changed: 362 additions & 10 deletions

File tree

frontend/src/components/console/project/issue-design-dialog.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { useCommonData } from "@/components/console/data-provider"
33
import { Button } from "@/components/ui/button"
44
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
55
import { Spinner } from "@/components/ui/spinner"
6-
import { selectHost, selectImage } from "@/utils/common"
76
import { apiRequest } from "@/utils/requestUtils"
87
import { IconSparkles } from "@tabler/icons-react"
98
import { useMemo, useState } from "react"
109
import { useNavigate } from "react-router-dom"
1110
import { TaskConcurrentLimitDialog } from "@/components/console/task/task-concurrent-limit-dialog"
12-
import { IssueTaskModelSelect, IssueTaskProjectFields, useIssueTaskModelSelection, useProjectBranchSelection } from "./issue-task-dialog-shared"
11+
import { IssueTaskHostSelect, IssueTaskImageSelect, IssueTaskModelSelect, IssueTaskProjectFields, useIssueTaskHostSelection, useIssueTaskImageSelection, useIssueTaskModelSelection, useProjectBranchSelection } from "./issue-task-dialog-shared"
1312
import { toast } from "sonner"
1413
import { useTranslation } from "react-i18next"
1514

@@ -36,6 +35,8 @@ export default function IssueDesignDialog({
3635
const { images, models, hosts, reloadProjects, reloadUnlinkedTasks, subscription } = useCommonData()
3736
const { branches, loadingBranches, selectedBranch, selectBranch } = useProjectBranchSelection(open, project)
3837
const { selectedModel, selectedModelId, setSelectedModelId } = useIssueTaskModelSelection(open, models, subscription)
38+
const { selectedHostId, setSelectedHostId } = useIssueTaskHostSelection(open, hosts, selectedModel)
39+
const { selectedImageId, setSelectedImageId } = useIssueTaskImageSelection(open, images, project?.image_id)
3940
const { t } = useTranslation()
4041

4142
const handleOpenChange = (open: boolean) => {
@@ -69,14 +70,24 @@ ${issue?.requirement_document?.replaceAll("`", "\\`")}
6970
return
7071
}
7172

73+
if (!selectedImageId) {
74+
toast.error(t("consoleProject.issueTask.toast.imageRequired"))
75+
return
76+
}
77+
78+
if (!selectedHostId) {
79+
toast.error(t("consoleProject.issueTask.toast.hostRequired"))
80+
return
81+
}
82+
7283
setSubmitting(true)
7384

7485
await apiRequest('v1UsersTasksCreate', {
7586
content: renderPrompt,
7687
cli_name: ConstsCliName.CliNameOpencode,
7788
model_id: selectedModelId,
78-
image_id: selectImage(images, false),
79-
host_id: selectHost(hosts, false),
89+
image_id: selectedImageId,
90+
host_id: selectedHostId,
8091
repo: {
8192
branch: project?.platform === ConstsGitPlatform.GitPlatformInternal ? '' : selectedBranch,
8293
},
@@ -134,6 +145,17 @@ ${issue?.requirement_document?.replaceAll("`", "\\`")}
134145
setSelectedModelId={setSelectedModelId}
135146
subscription={subscription}
136147
/>
148+
<IssueTaskHostSelect
149+
hosts={hosts}
150+
selectedHostId={selectedHostId}
151+
selectedModel={selectedModel}
152+
setSelectedHostId={setSelectedHostId}
153+
/>
154+
<IssueTaskImageSelect
155+
images={images}
156+
selectedImageId={selectedImageId}
157+
setSelectedImageId={setSelectedImageId}
158+
/>
137159
</div>
138160
<DialogFooter className="shrink-0 border-t pt-4">
139161
<Button onClick={handleConfirm} disabled={submitting}>

frontend/src/components/console/project/issue-dev-dialog.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { useCommonData } from "@/components/console/data-provider"
33
import { Button } from "@/components/ui/button"
44
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
55
import { Spinner } from "@/components/ui/spinner"
6-
import { selectHost, selectImage } from "@/utils/common"
76
import { apiRequest } from "@/utils/requestUtils"
87
import { IconSparkles } from "@tabler/icons-react"
98
import { useMemo, useState } from "react"
109
import { useNavigate } from "react-router-dom"
1110
import { TaskConcurrentLimitDialog } from "@/components/console/task/task-concurrent-limit-dialog"
12-
import { IssueTaskModelSelect, IssueTaskProjectFields, useIssueTaskModelSelection, useProjectBranchSelection } from "./issue-task-dialog-shared"
11+
import { IssueTaskHostSelect, IssueTaskImageSelect, IssueTaskModelSelect, IssueTaskProjectFields, useIssueTaskHostSelection, useIssueTaskImageSelection, useIssueTaskModelSelection, useProjectBranchSelection } from "./issue-task-dialog-shared"
1312
import { toast } from "sonner"
1413
import { useTranslation } from "react-i18next"
1514

@@ -36,6 +35,8 @@ export default function IssueDevelopDialog({
3635
const { images, models, hosts, reloadProjects, reloadUnlinkedTasks, subscription } = useCommonData()
3736
const { branches, loadingBranches, selectedBranch, selectBranch } = useProjectBranchSelection(open, project)
3837
const { selectedModel, selectedModelId, setSelectedModelId } = useIssueTaskModelSelection(open, models, subscription)
38+
const { selectedHostId, setSelectedHostId } = useIssueTaskHostSelection(open, hosts, selectedModel)
39+
const { selectedImageId, setSelectedImageId } = useIssueTaskImageSelection(open, images, project?.image_id)
3940
const { t } = useTranslation()
4041

4142
const handleOpenChange = (open: boolean) => {
@@ -75,14 +76,24 @@ ${issue?.design_document?.replaceAll("`", "\\`")}
7576
return
7677
}
7778

79+
if (!selectedImageId) {
80+
toast.error(t("consoleProject.issueTask.toast.imageRequired"))
81+
return
82+
}
83+
84+
if (!selectedHostId) {
85+
toast.error(t("consoleProject.issueTask.toast.hostRequired"))
86+
return
87+
}
88+
7889
setSubmitting(true)
7990

8091
await apiRequest('v1UsersTasksCreate', {
8192
content: renderPrompt,
8293
cli_name: ConstsCliName.CliNameOpencode,
8394
model_id: selectedModelId,
84-
image_id: selectImage(images, false),
85-
host_id: selectHost(hosts, false),
95+
image_id: selectedImageId,
96+
host_id: selectedHostId,
8697
repo: {
8798
branch: project?.platform === ConstsGitPlatform.GitPlatformInternal ? '' : selectedBranch,
8899
},
@@ -139,6 +150,17 @@ ${issue?.design_document?.replaceAll("`", "\\`")}
139150
setSelectedModelId={setSelectedModelId}
140151
subscription={subscription}
141152
/>
153+
<IssueTaskHostSelect
154+
hosts={hosts}
155+
selectedHostId={selectedHostId}
156+
selectedModel={selectedModel}
157+
setSelectedHostId={setSelectedHostId}
158+
/>
159+
<IssueTaskImageSelect
160+
images={images}
161+
selectedImageId={selectedImageId}
162+
setSelectedImageId={setSelectedImageId}
163+
/>
142164
</div>
143165
<DialogFooter className="shrink-0 border-t pt-4">
144166
<Button onClick={handleConfirm} disabled={submitting}>

frontend/src/components/console/project/issue-task-dialog-shared.tsx

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { ConstsGitPlatform, type DomainBranch, type DomainModel, type DomainProject, type DomainSubscriptionResp } from "@/api/Api"
1+
import { ConstsGitPlatform, ConstsHostStatus, ConstsOwnerType, type DomainBranch, type DomainHost, type DomainImage, type DomainModel, type DomainProject, type DomainSubscriptionResp } from "@/api/Api"
2+
import Icon from "@/components/common/Icon"
3+
import { Badge } from "@/components/ui/badge"
24
import { Input } from "@/components/ui/input"
35
import { Label } from "@/components/ui/label"
46
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
57
import { Spinner } from "@/components/ui/spinner"
8+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
69
import ModelSelect from "@/components/console/task/model-select"
7-
import { selectPreferredTaskModel } from "@/utils/common"
10+
import { getHostBadges, getImageShortName, getOSFromImageName, getOwnerTypeBadge, selectHost, selectImage, selectPreferredTaskModel } from "@/utils/common"
11+
import { IS_OFFLINE_EDITION } from "@/utils/edition"
812
import { apiRequest } from "@/utils/requestUtils"
913
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
1014
import { toast } from "sonner"
@@ -62,6 +66,76 @@ export function useIssueTaskModelSelection(
6266
}
6367
}
6468

69+
export function useIssueTaskImageSelection(
70+
open: boolean,
71+
images: DomainImage[],
72+
projectImageId?: string,
73+
) {
74+
const [selectedImageId, setSelectedImageId] = useState("")
75+
76+
useEffect(() => {
77+
if (!open) {
78+
setSelectedImageId("")
79+
return
80+
}
81+
82+
const selectedImageIsValid = images.some((image) => image.id === selectedImageId)
83+
if (selectedImageIsValid) {
84+
return
85+
}
86+
87+
const projectImageIsValid = !!projectImageId && images.some((image) => image.id === projectImageId)
88+
setSelectedImageId(projectImageIsValid ? projectImageId : selectImage(images, true))
89+
}, [images, open, projectImageId, selectedImageId])
90+
91+
return {
92+
selectedImageId,
93+
setSelectedImageId,
94+
}
95+
}
96+
97+
function getDefaultIssueTaskHostId(hosts: DomainHost[]) {
98+
if (IS_OFFLINE_EDITION) {
99+
return hosts.find((host) => host.id && host.status === ConstsHostStatus.HostStatusOnline)?.id || ""
100+
}
101+
102+
return selectHost(hosts, false)
103+
}
104+
105+
export function useIssueTaskHostSelection(
106+
open: boolean,
107+
hosts: DomainHost[],
108+
selectedModel?: DomainModel,
109+
) {
110+
const [selectedHostId, setSelectedHostId] = useState("")
111+
const selectedPublicModel = selectedModel?.owner?.type === ConstsOwnerType.OwnerTypePublic
112+
113+
useEffect(() => {
114+
if (!open) {
115+
setSelectedHostId("")
116+
return
117+
}
118+
119+
if (!IS_OFFLINE_EDITION && selectedPublicModel) {
120+
setSelectedHostId("public_host")
121+
return
122+
}
123+
124+
const selectedHostIsValid = selectedHostId === "public_host"
125+
? !IS_OFFLINE_EDITION
126+
: hosts.some((host) => host.id === selectedHostId && host.status === ConstsHostStatus.HostStatusOnline)
127+
128+
if (!selectedHostIsValid) {
129+
setSelectedHostId(getDefaultIssueTaskHostId(hosts))
130+
}
131+
}, [hosts, open, selectedHostId, selectedPublicModel])
132+
133+
return {
134+
selectedHostId,
135+
setSelectedHostId,
136+
}
137+
}
138+
65139
export function useProjectBranchSelection(open: boolean, project?: DomainProject) {
66140
const { t } = useTranslation()
67141
const [branches, setBranches] = useState<string[]>([])
@@ -248,3 +322,90 @@ export function IssueTaskModelSelect({
248322
</div>
249323
)
250324
}
325+
326+
export function IssueTaskImageSelect({
327+
images,
328+
selectedImageId,
329+
setSelectedImageId,
330+
}: {
331+
images: DomainImage[]
332+
selectedImageId: string
333+
setSelectedImageId: (imageId: string) => void
334+
}) {
335+
const { t } = useTranslation()
336+
337+
return (
338+
<div className="space-y-2">
339+
<Label>{t("consoleProject.issueTask.image")}</Label>
340+
<Select value={selectedImageId} onValueChange={setSelectedImageId}>
341+
<SelectTrigger className="w-full">
342+
<SelectValue placeholder={t("consoleProject.issueTask.selectImage")} />
343+
</SelectTrigger>
344+
<SelectContent>
345+
{images.filter((image) => image.id).map((image) => (
346+
<SelectItem key={image.id} value={image.id!}>
347+
<Tooltip>
348+
<TooltipTrigger asChild>
349+
<div className="flex items-center gap-2">
350+
<Icon name={getOSFromImageName(image.name || "")} className="size-4" />
351+
<span>{image.remark || getImageShortName(image.name || "")}</span>
352+
{getOwnerTypeBadge(image.owner)}
353+
</div>
354+
</TooltipTrigger>
355+
<TooltipContent side="right">{image.name}</TooltipContent>
356+
</Tooltip>
357+
</SelectItem>
358+
))}
359+
</SelectContent>
360+
</Select>
361+
</div>
362+
)
363+
}
364+
365+
export function IssueTaskHostSelect({
366+
hosts,
367+
selectedHostId,
368+
selectedModel,
369+
setSelectedHostId,
370+
}: {
371+
hosts: DomainHost[]
372+
selectedHostId: string
373+
selectedModel?: DomainModel
374+
setSelectedHostId: (hostId: string) => void
375+
}) {
376+
const { t } = useTranslation()
377+
const selectedPublicModel = selectedModel?.owner?.type === ConstsOwnerType.OwnerTypePublic
378+
379+
return (
380+
<div className="space-y-2">
381+
<Label>{t("consoleProject.issueTask.host")}</Label>
382+
<Select value={selectedHostId} onValueChange={setSelectedHostId}>
383+
<SelectTrigger className="w-full">
384+
<SelectValue placeholder={t("consoleProject.issueTask.selectHost")} />
385+
</SelectTrigger>
386+
<SelectContent>
387+
{!IS_OFFLINE_EDITION && (
388+
<SelectItem value="public_host">
389+
<div className="flex items-center gap-2">
390+
<span>MonkeyCode</span>
391+
<Badge className="!text-primary-foreground">{t("consoleProject.issueTask.free")}</Badge>
392+
</div>
393+
</SelectItem>
394+
)}
395+
{hosts.filter((host) => host.id).map((host) => (
396+
<SelectItem
397+
key={host.id}
398+
value={host.id!}
399+
disabled={host.status !== ConstsHostStatus.HostStatusOnline || (!IS_OFFLINE_EDITION && selectedPublicModel)}
400+
>
401+
<div className="flex items-center gap-2">
402+
<span>{host.remark || `${host.name}-${host.external_ip}`}</span>
403+
{getHostBadges(host)}
404+
</div>
405+
</SelectItem>
406+
))}
407+
</SelectContent>
408+
</Select>
409+
</div>
410+
)
411+
}

frontend/src/components/console/task/create-default-task-dialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ export default function CreateDefaultTaskDialog({
313313
return
314314
}
315315

316+
if (!selectedImageId) {
317+
toast.error(t("taskWorkflow.toast.missingImage"))
318+
return
319+
}
320+
316321
if (!selectedRepoDisplayName.endsWith(".zip") && selectedRepo && !selectedIdentityId) {
317322
setSelectedIdentityId("none")
318323
}

frontend/src/components/console/task/task-input.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ export function TaskInput({ repos, onTaskCreated }: TaskInputProps) {
184184
return;
185185
}
186186

187+
if (!selectedImageId) {
188+
toast.error(t("taskWorkflow.toast.missingImage"));
189+
return;
190+
}
191+
187192
if (!IS_OFFLINE_EDITION && selectedModel?.owner?.type === ConstsOwnerType.OwnerTypePublic && selectedHostId !== "public_host") {
188193
toast.warning(t("taskWorkflow.toast.builtinModelHostOnly"));
189194
return;

frontend/src/i18n/resources/cn.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,12 +635,19 @@ const cn = {
635635
branch: "选择分支",
636636
selectBranch: "请选择分支",
637637
model: "大模型",
638+
image: "系统镜像",
639+
selectImage: "请选择系统镜像",
640+
host: "宿主机",
641+
selectHost: "请选择宿主机",
642+
free: "免费",
638643
toast: {
639644
repositoryUnavailable: "无法获取仓库信息",
640645
fetchBranchesFailedWithMessage: "获取分支列表失败: {{message}}",
641646
fetchBranchesFailed: "获取分支列表失败",
642647
branchRequired: "请选择分支",
643648
modelRequired: "请选择大模型",
649+
imageRequired: "请选择系统镜像",
650+
hostRequired: "请选择宿主机",
644651
startFailed: "任务启动失败",
645652
},
646653
design: {
@@ -3863,6 +3870,7 @@ const cn = {
38633870
fetchPluginsFailed: "获取插件列表失败",
38643871
missingContent: "请输入任务内容",
38653872
missingModel: "请选择大模型",
3873+
missingImage: "请选择系统镜像",
38663874
builtinModelHostOnly: "内置模型只能在内置宿主机上使用",
38673875
taskStarted: "任务启动成功",
38683876
taskStartFailed: "任务启动失败",

0 commit comments

Comments
 (0)