feat: complete TASK-WP2-05 generation submission
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Generated from openapi/openapi.json. Do not edit by hand.
|
||||
|
||||
import type { CreditAdjustmentResponse, CreditAdjustmentRequest, AccountDeletionResponse, AccountDeletionCompleteRequest, AdminLoginCompleteResponse, AdminLoginCompleteRequest, LoginCompleteResponse, LoginCompleteRequest, RegistrationCompleteResponse, RegistrationCompleteRequest, AccountSettingsResponse, AdminSessionResponse, CreditBalanceResponse, CreditLedgerResponse, ProjectDetailResponse, UserSessionResponse, ProjectListResponse, LogoutResponse, ProjectPurgeResponse, ProjectRenameResponse, ProjectRenameRequest, ProjectRestoreResponse, ProjectStateSaveResponse, ProjectEditableState, AccountDeletionSendResponse, RegistrationSendResponse, AdminLoginSendRequest, LoginSendRequest, RegistrationSendRequest, FailedEmptyTrashResponse, FailedEmptyTrashRequest, ProjectTrashResponse, AccountProfileUpdateResponse, AccountProfileUpdateRequest } from "./types.gen.js";
|
||||
import type { CreditAdjustmentResponse, CreditAdjustmentRequest, AccountDeletionResponse, AccountDeletionCompleteRequest, AdminLoginCompleteResponse, AdminLoginCompleteRequest, LoginCompleteResponse, LoginCompleteRequest, RegistrationCompleteResponse, RegistrationCompleteRequest, GenerationCreateResponse, AccountSettingsResponse, AdminSessionResponse, CreditBalanceResponse, GenerationTaskResponse, CreditLedgerResponse, ProjectDetailResponse, UserSessionResponse, ProjectListResponse, LogoutResponse, ProjectPurgeResponse, ProjectRenameResponse, ProjectRenameRequest, ProjectRestoreResponse, ProjectStateSaveResponse, ProjectEditableState, AccountDeletionSendResponse, RegistrationSendResponse, AdminLoginSendRequest, LoginSendRequest, RegistrationSendRequest, FailedEmptyTrashResponse, FailedEmptyTrashRequest, ProjectTrashResponse, AccountProfileUpdateResponse, AccountProfileUpdateRequest } from "./types.gen.js";
|
||||
|
||||
export interface ClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; headers?: HeadersInit; }
|
||||
|
||||
@@ -90,6 +90,13 @@ export async function completeRegistration(body: RegistrationCompleteRequest, op
|
||||
return response.json() as Promise<RegistrationCompleteResponse>;
|
||||
}
|
||||
|
||||
export async function createGeneration(options: ClientOptions = {}): Promise<GenerationCreateResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/generations`, { method: "POST", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<GenerationCreateResponse>;
|
||||
}
|
||||
|
||||
export async function getAccountSettings(options: ClientOptions = {}): Promise<AccountSettingsResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/account/settings`, { method: "GET", headers: options.headers ?? {} });
|
||||
@@ -150,10 +157,24 @@ export async function getBootstrap(options: ClientOptions = {}): Promise<{
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getCurrentGeneration(options: ClientOptions = {}): Promise<GenerationTaskResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/generations/current`, { method: "GET", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<GenerationTaskResponse>;
|
||||
}
|
||||
|
||||
export function getEvents(options: Pick<ClientOptions, "baseUrl"> = {}): string {
|
||||
return `${options.baseUrl ?? ""}/api/v1/events`;
|
||||
}
|
||||
|
||||
export async function getGeneration(options: ClientOptions = {}): Promise<GenerationTaskResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/generations/{generationId}`, { method: "GET", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<GenerationTaskResponse>;
|
||||
}
|
||||
|
||||
export async function getMyCreditLedger(options: ClientOptions = {}): Promise<CreditLedgerResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/me/credit-ledger`, { method: "GET", headers: options.headers ?? {} });
|
||||
|
||||
@@ -320,8 +320,36 @@ export type FailedEmptyTrashResponse = {
|
||||
"trashed_project_ids": Array<ProjectId>;
|
||||
};
|
||||
|
||||
export type GenerationCreateHeaders = {
|
||||
"idempotency-key": string;
|
||||
"x-csrf-token": string;
|
||||
};
|
||||
|
||||
export type GenerationCreateResponse = {
|
||||
"created": boolean;
|
||||
"task": GenerationTaskResponse;
|
||||
};
|
||||
|
||||
export type GenerationErrorCategory = "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled" | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid" | "unknown_retryable" | "unknown_non_retryable";
|
||||
|
||||
export type GenerationMultipartBody = {
|
||||
"client_submission_id": string;
|
||||
"confirmed_credit_cost": string;
|
||||
"creation_mode": "new_project" | "existing_project";
|
||||
"existing_reference_asset_ids"?: string;
|
||||
"model_config_version": string;
|
||||
"model_id": string;
|
||||
"project_id"?: string;
|
||||
"prompt": string;
|
||||
"ratio": ProjectRatio;
|
||||
"reference_files"?: Array<string>;
|
||||
"reference_manifest"?: string;
|
||||
};
|
||||
|
||||
export type GenerationParams = {
|
||||
"generationId": string;
|
||||
};
|
||||
|
||||
export type GenerationProjectItem = {
|
||||
"created_at": string;
|
||||
"error_category": GenerationErrorCategory | null;
|
||||
@@ -332,6 +360,23 @@ export type GenerationProjectItem = {
|
||||
"updated_at": string;
|
||||
};
|
||||
|
||||
export type GenerationTaskResponse = {
|
||||
"confirmed_credit_cost": number;
|
||||
"created_at": string;
|
||||
"generation_id": string;
|
||||
"model_config_version": number;
|
||||
"model_id": string;
|
||||
"project_id": ProjectId;
|
||||
"prompt": string;
|
||||
"ratio": ProjectRatio;
|
||||
"reference_count": number;
|
||||
"reserved_credits": number;
|
||||
"status": GenerationTaskStatus;
|
||||
"updated_at": string;
|
||||
};
|
||||
|
||||
export type GenerationTaskStatus = "queued" | "running" | "succeeded" | "failed" | "rejected";
|
||||
|
||||
export type LoginCompleteRequest = {
|
||||
"registration_id": string;
|
||||
"verification_code": string;
|
||||
|
||||
@@ -370,6 +370,93 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.current-task-active {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
align-content: start;
|
||||
margin-top: 28px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #a6a6a0;
|
||||
}
|
||||
|
||||
.current-task-active .task-state {
|
||||
justify-self: start;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #111111;
|
||||
background: #f2f500;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.current-task-active > strong {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.current-task-active dl {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
border-top: 1px solid #b6b6b0;
|
||||
}
|
||||
|
||||
.current-task-active dl > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid #b6b6b0;
|
||||
}
|
||||
|
||||
.current-task-active dt {
|
||||
color: #65655f;
|
||||
}
|
||||
|
||||
.current-task-active dd {
|
||||
margin: 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.current-task-active a {
|
||||
color: #111111;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.capacity-critical,
|
||||
.generation-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #111111;
|
||||
}
|
||||
|
||||
.capacity-critical {
|
||||
background: #ffdf66;
|
||||
}
|
||||
|
||||
.capacity-critical span,
|
||||
.generation-notice span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.generation-notice {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.generation-notice button {
|
||||
flex: 0 0 auto;
|
||||
min-height: 38px;
|
||||
padding: 7px 12px;
|
||||
border: 1px solid #111111;
|
||||
background: #f2f500;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.recent-projects {
|
||||
grid-column: 1;
|
||||
margin-top: 28px;
|
||||
@@ -1167,6 +1254,12 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.capacity-critical,
|
||||
.generation-notice {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.projects-title {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
+207
-10
@@ -16,9 +16,53 @@ type ProjectStatus = "active" | "failed_empty" | "trashed";
|
||||
interface SessionPayload {
|
||||
credits: { available_balance: number; reserved_balance: number };
|
||||
csrf_token: string;
|
||||
local_data?: LocalDataPayload;
|
||||
user: { creator_name: string };
|
||||
}
|
||||
|
||||
interface LocalDataPayload {
|
||||
capacity_status: "normal" | "warning" | "critical" | "full" | "unavailable";
|
||||
hard_limit_bytes: number;
|
||||
managed_content_bytes: number;
|
||||
}
|
||||
|
||||
interface AccountSettingsPayload {
|
||||
local_data: LocalDataPayload;
|
||||
}
|
||||
|
||||
interface ModelPayload {
|
||||
config_set_version: number;
|
||||
configured_default_model_id: string;
|
||||
models: Array<{
|
||||
config_version: number;
|
||||
contract_validation_status: "verified" | "unverified";
|
||||
credit_cost: number;
|
||||
enabled: boolean;
|
||||
is_default: boolean;
|
||||
model_id: string;
|
||||
prompt_max_length: number;
|
||||
reference_limits: { max_file_bytes: number; max_files: number; max_total_bytes: number };
|
||||
runtime_availability: { available_for_new_jobs: boolean; checked_at: string; reason: string | null };
|
||||
supported_ratios: Ratio[];
|
||||
}>;
|
||||
recommended_model_id: string | null;
|
||||
}
|
||||
|
||||
interface GenerationTaskPayload {
|
||||
confirmed_credit_cost: number;
|
||||
created_at: string;
|
||||
generation_id: string;
|
||||
model_config_version: number;
|
||||
model_id: string;
|
||||
project_id: string;
|
||||
prompt: string;
|
||||
ratio: Ratio;
|
||||
reference_count: number;
|
||||
reserved_credits: number;
|
||||
status: "queued" | "running" | "succeeded" | "failed" | "rejected";
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ProjectSummary {
|
||||
current_image_id: string | null;
|
||||
deleted_at?: string | null;
|
||||
@@ -67,6 +111,17 @@ async function readJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function readOptionalJson<T>(url: string): Promise<T | undefined> {
|
||||
const response = await fetch(url, { credentials: "same-origin" });
|
||||
if (response.status === 401) {
|
||||
window.dispatchEvent(new Event("dada:session-invalid"));
|
||||
throw new Error("session_invalid");
|
||||
}
|
||||
if (response.status === 404) return undefined;
|
||||
if (!response.ok) throw new Error("request_failed");
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
function formatUpdatedAt(value: string) {
|
||||
return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||
}
|
||||
@@ -151,10 +206,17 @@ export function WorkspacePage() {
|
||||
const promptId = useId();
|
||||
const [session, setSession] = useState<SessionPayload>();
|
||||
const [projects, setProjects] = useState<ProjectListPayload>();
|
||||
const [models, setModels] = useState<ModelPayload>();
|
||||
const [currentTask, setCurrentTask] = useState<GenerationTaskPayload>();
|
||||
const [localData, setLocalData] = useState<LocalDataPayload>();
|
||||
const [generationStateLoaded, setGenerationStateLoaded] = useState(false);
|
||||
const [loadingFailed, setLoadingFailed] = useState(false);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [ratio, setRatio] = useState<Ratio>("3:4");
|
||||
const [references, setReferences] = useState<string[]>([]);
|
||||
const [references, setReferences] = useState<File[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [generationNotice, setGenerationNotice] = useState("");
|
||||
const [requiresReconfirmation, setRequiresReconfirmation] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -165,12 +227,113 @@ export function WorkspacePage() {
|
||||
if (!active) return;
|
||||
setSession(nextSession);
|
||||
setProjects(nextProjects);
|
||||
setLocalData(nextSession.local_data);
|
||||
return Promise.allSettled([
|
||||
readOptionalJson<ModelPayload>("/api/v1/models"),
|
||||
readOptionalJson<GenerationTaskPayload>("/api/v1/generations/current"),
|
||||
readOptionalJson<AccountSettingsPayload>("/api/v1/account/settings"),
|
||||
]).then(([modelResult, taskResult, settingsResult]) => {
|
||||
if (!active) return;
|
||||
if (modelResult.status === "fulfilled") setModels(modelResult.value);
|
||||
if (taskResult.status === "fulfilled") setCurrentTask(taskResult.value);
|
||||
if (settingsResult.status === "fulfilled" && settingsResult.value) setLocalData(settingsResult.value.local_data);
|
||||
setGenerationStateLoaded(true);
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (active && error instanceof Error && error.message !== "session_invalid") setLoadingFailed(true);
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
const selectedModel = useMemo(() => {
|
||||
if (!models) return undefined;
|
||||
const modelId = models.recommended_model_id ?? models.configured_default_model_id;
|
||||
return models.models.find((model) => model.model_id === modelId && model.enabled
|
||||
&& model.contract_validation_status === "verified" && model.runtime_availability.available_for_new_jobs);
|
||||
}, [models]);
|
||||
|
||||
const referenceBytes = references.reduce((sum, file) => sum + file.size, 0);
|
||||
const referencesValid = selectedModel !== undefined
|
||||
&& references.length <= selectedModel.reference_limits.max_files
|
||||
&& referenceBytes <= selectedModel.reference_limits.max_total_bytes
|
||||
&& references.every((file) => file.size > 0 && file.size <= selectedModel.reference_limits.max_file_bytes);
|
||||
const capacityBlocksGeneration = localData?.capacity_status === "full" || localData?.capacity_status === "unavailable";
|
||||
const canSubmit = generationStateLoaded && !currentTask && !submitting && !requiresReconfirmation && selectedModel !== undefined
|
||||
&& prompt.trim().length > 0 && prompt.trim().length <= selectedModel.prompt_max_length
|
||||
&& selectedModel.supported_ratios.includes(ratio) && referencesValid
|
||||
&& session !== undefined && session.credits.available_balance >= selectedModel.credit_cost && !capacityBlocksGeneration;
|
||||
|
||||
async function submitGeneration(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!session || !selectedModel || !canSubmit) return;
|
||||
const body = new FormData();
|
||||
body.append("client_submission_id", crypto.randomUUID());
|
||||
body.append("confirmed_credit_cost", String(selectedModel.credit_cost));
|
||||
body.append("creation_mode", "new_project");
|
||||
body.append("existing_reference_asset_ids", "[]");
|
||||
body.append("model_config_version", String(selectedModel.config_version));
|
||||
body.append("model_id", selectedModel.model_id);
|
||||
body.append("prompt", prompt.trim());
|
||||
body.append("ratio", ratio);
|
||||
body.append("reference_manifest", JSON.stringify(references.map((file) => ({
|
||||
file_name: file.name,
|
||||
mime_type: file.type,
|
||||
size: file.size,
|
||||
}))));
|
||||
for (const file of references) body.append("reference_files", file, file.name);
|
||||
setSubmitting(true);
|
||||
setGenerationNotice("");
|
||||
try {
|
||||
const response = await fetch("/api/v1/generations", {
|
||||
body,
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Idempotency-Key": `generation-${crypto.randomUUID()}`,
|
||||
"X-CSRF-Token": session.csrf_token,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
window.dispatchEvent(new Event("dada:session-invalid"));
|
||||
return;
|
||||
}
|
||||
if (response.status === 412) {
|
||||
setRequiresReconfirmation(true);
|
||||
setGenerationNotice("模型配置已更新,请确认最新配置后重新提交。");
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
setGenerationNotice(response.status === 507 ? "本机存储空间不足,当前不能创建新任务。" : "任务未提交,请检查当前状态后重试。");
|
||||
return;
|
||||
}
|
||||
const result = await response.json() as { created: boolean; task: GenerationTaskPayload };
|
||||
setCurrentTask(result.task);
|
||||
setSession((current) => current ? {
|
||||
...current,
|
||||
credits: {
|
||||
available_balance: current.credits.available_balance - (result.created ? result.task.reserved_credits : 0),
|
||||
reserved_balance: current.credits.reserved_balance + (result.created ? result.task.reserved_credits : 0),
|
||||
},
|
||||
} : current);
|
||||
setGenerationNotice(result.created ? "任务已提交。" : "已返回当前进行中的任务。");
|
||||
} catch {
|
||||
setGenerationNotice("任务未提交,请检查本机服务后重试。");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmLatestModelConfiguration() {
|
||||
try {
|
||||
const next = await readJson<ModelPayload>("/api/v1/models");
|
||||
setModels(next);
|
||||
setRequiresReconfirmation(false);
|
||||
setGenerationNotice("已确认最新配置,请重新检查点数和参考图后提交。");
|
||||
} catch {
|
||||
setGenerationNotice("暂时无法读取最新模型配置。");
|
||||
}
|
||||
}
|
||||
|
||||
if (loadingFailed) {
|
||||
return (
|
||||
<main className="product-loading">
|
||||
@@ -197,7 +360,7 @@ export function WorkspacePage() {
|
||||
</section>
|
||||
) : null}
|
||||
<main className={`workspace-main ${empty ? "is-empty" : "has-projects"}`}>
|
||||
<section className="generation-area" aria-labelledby={empty ? undefined : "workspace-compact-title"}>
|
||||
<form className="generation-area" aria-labelledby={empty ? undefined : "workspace-compact-title"} onSubmit={submitGeneration}>
|
||||
{!empty ? (
|
||||
<div className="workspace-compact-heading">
|
||||
<div><p>NEW PROJECT</p><h1 id="workspace-compact-title">新建创作</h1></div>
|
||||
@@ -215,14 +378,21 @@ export function WorkspacePage() {
|
||||
<div className="generation-options">
|
||||
<div className="model-status" aria-live="polite">
|
||||
<span>模型</span>
|
||||
<strong>当前没有可用于新任务的模型</strong>
|
||||
<strong>{selectedModel ? selectedModel.model_id : "当前没有可用于新任务的模型"}</strong>
|
||||
</div>
|
||||
<fieldset className="ratio-control">
|
||||
<legend>画面比例</legend>
|
||||
<div>
|
||||
{(["3:4", "1:1", "4:3", "9:16"] as const).map((value) => (
|
||||
<label key={value}>
|
||||
<input checked={ratio === value} name="ratio" onChange={() => setRatio(value)} type="radio" value={value} />
|
||||
<input
|
||||
checked={ratio === value}
|
||||
disabled={selectedModel !== undefined && !selectedModel.supported_ratios.includes(value)}
|
||||
name="ratio"
|
||||
onChange={() => setRatio(value)}
|
||||
type="radio"
|
||||
value={value}
|
||||
/>
|
||||
<span>{value}</span>
|
||||
</label>
|
||||
))}
|
||||
@@ -231,22 +401,49 @@ export function WorkspacePage() {
|
||||
</div>
|
||||
<label className="reference-input">
|
||||
<span>添加参考图</span>
|
||||
<small>{references.length > 0 ? references.join("、") : "尚未提交,仅保留在当前页面"}</small>
|
||||
<small>{references.length > 0 ? references.map((file) => file.name).join("、") : "尚未提交,仅保留在当前页面"}</small>
|
||||
<input
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
onChange={(event) => setReferences(Array.from(event.target.files ?? []).map((file) => file.name))}
|
||||
onChange={(event) => setReferences(Array.from(event.target.files ?? []))}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
{localData?.capacity_status === "critical" ? (
|
||||
<div className="capacity-critical" role="status">
|
||||
<strong>存储空间已超过 90%</strong>
|
||||
<span>请尽快清理本机内容,达到上限后将无法提交新任务。</span>
|
||||
</div>
|
||||
) : null}
|
||||
{generationNotice ? (
|
||||
<div className="generation-notice" role="status">
|
||||
<span>{generationNotice}</span>
|
||||
{requiresReconfirmation ? (
|
||||
<button onClick={confirmLatestModelConfiguration} type="button">确认最新配置</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="generation-submit">
|
||||
<span>预计点数将在模型可用后显示</span>
|
||||
<button disabled type="button">生成一张图片</button>
|
||||
<span>{selectedModel ? `本次预计冻结 ${selectedModel.credit_cost} 点` : "预计点数将在模型可用后显示"}</span>
|
||||
<button disabled={!canSubmit} type="submit">{submitting ? "正在提交" : "生成一张图片"}</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<aside className="current-task" aria-labelledby="current-task-title">
|
||||
<h2 id="current-task-title">当前任务</h2>
|
||||
<div className="current-task-empty"><i /><strong>没有进行中的任务</strong><span>任务状态会持续显示在这里</span></div>
|
||||
{currentTask ? (
|
||||
<div className="current-task-active">
|
||||
<span className="task-state">{currentTask.status === "queued" ? "排队中" : "处理中"}</span>
|
||||
<strong>{currentTask.prompt}</strong>
|
||||
<dl>
|
||||
<div><dt>画面比例</dt><dd>{currentTask.ratio}</dd></div>
|
||||
<div><dt>参考图</dt><dd>{currentTask.reference_count} 张</dd></div>
|
||||
<div><dt>点数</dt><dd>已冻结 {currentTask.reserved_credits} 点</dd></div>
|
||||
</dl>
|
||||
<a href={`/app/projects/${currentTask.project_id}`}>返回当前项目</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="current-task-empty"><i /><strong>没有进行中的任务</strong><span>任务状态会持续显示在这里</span></div>
|
||||
)}
|
||||
</aside>
|
||||
{!empty ? (
|
||||
<section className="recent-projects" aria-labelledby="recent-title">
|
||||
|
||||
Reference in New Issue
Block a user