diff --git a/service/index.html b/service/index.html index 0b0fb88..372698d 100644 --- a/service/index.html +++ b/service/index.html @@ -2213,6 +2213,10 @@ A+B design_target_target: "目标浓度", design_tube_defect: "Tube 缺陷", design_complex_defect: "Complex 缺陷", + design_analyze_direct: "直接分析设计结果", + design_load_analysis: "载入为 Analysis 输入", + design_analysis_loaded: "已载入设计序列,可修改链浓度后分析", + design_analysis_unavailable: "没有可分析的设计结果", details_json: "展开原始 JSON", toggle_structure_data: "结构数据", toggle_pairs_panel: "配对热图", @@ -2517,6 +2521,10 @@ A+B design_target_target: "Target Concentration", design_tube_defect: "Tube Defect", design_complex_defect: "Complex Defect", + design_analyze_direct: "Analyze Designed Result", + design_load_analysis: "Load as Analysis Input", + design_analysis_loaded: "Designed sequences loaded; adjust strand concentrations before analysis", + design_analysis_unavailable: "No designed result is available for analysis", details_json: "Show raw JSON", toggle_structure_data: "Structure Data", toggle_pairs_panel: "Pair Heatmap", @@ -3747,6 +3755,81 @@ A+B }; } + function buildAnalysisOptionsFromControls() { + return { + num_sample: Number(document.getElementById("numSample").value), + energy_gap: Number(document.getElementById("energyGap").value), + sparsity_fraction: Number(document.getElementById("sparsityFraction").value), + sparsity_threshold: Number(document.getElementById("sparsityThreshold").value), + single_mfe: false, + result_limit: Number(document.getElementById("resultLimit").value), + pairs_preview_size: Number(document.getElementById("pairsPreviewSize").value), + }; + } + + function buildDesignedAnalysisPayload(result) { + if (!result || workflowKind(result) !== "design" || !Array.isArray(result.strands) || !result.strands.length) { + throw new Error(t("design_analysis_unavailable")); + } + const inputStrandsByName = new Map((lastPayload?.strands || []).map((strand) => [strand.name, strand])); + const strands = result.strands.map((strand) => { + const input = inputStrandsByName.get(strand.name) || {}; + return { + name: strand.name, + sequence: strand.sequence, + concentration: Number(input.concentration ?? 1.0), + unit: input.unit || "uM", + }; + }); + const targetLines = (result.design?.targets || result.complexes || []) + .map((target) => Array.isArray(target.strand_names) ? target.strand_names.join("+") : "") + .filter(Boolean); + return { + workflow: "analysis", + mode: result.mode || "tube", + model: { ...result.model }, + compute: ["pfunc", "mfe", "pairs"], + options: buildAnalysisOptionsFromControls(), + strands, + tube: { + name: result.tube?.name || result.design?.target_tubes?.[0]?.name || "designed_tube", + max_size: Number(result.tube?.max_size ?? result.design?.target_tubes?.[0]?.max_size ?? 2), + }, + complexes_text: targetLines.join("\n"), + design_domains: [], + design_complexes: [], + design_tubes: [], + design: {}, + hard_constraints: [], + soft_constraints: [], + defect_weights: [], + }; + } + + function loadDesignedAnalysisInput(result) { + const payload = buildDesignedAnalysisPayload(result); + workflowSelect.value = "analysis"; + modeSelect.value = payload.mode; + document.getElementById("material").value = payload.model.material; + document.getElementById("ensemble").value = payload.model.ensemble; + document.getElementById("celsius").value = payload.model.celsius; + document.getElementById("sodium").value = payload.model.sodium; + document.getElementById("magnesium").value = payload.model.magnesium; + document.getElementById("tubeName").value = payload.tube.name; + document.getElementById("maxSize").value = payload.tube.max_size; + document.querySelectorAll(".check input").forEach((input) => { + input.checked = payload.compute.includes(input.value); + }); + strandList.innerHTML = ""; + payload.strands.forEach((strand) => addStrandRow(strand)); + syncComplexRowsFromText(payload.complexes_text); + syncWorkflow(); + syncMode(); + document.querySelector(".main-grid")?.scrollIntoView({ behavior: "smooth", block: "start" }); + setStatus(formatStatus(t("design_analysis_loaded"))); + return payload; + } + function renderModelCard(result) { return `
@@ -3864,6 +3947,10 @@ A+B
${t("design_seed")}${stats.seed ?? "-"}
${t("summary_elapsed")}${stats.design_time ? `${Number(stats.design_time).toFixed(3)} s` : "-"}
+
+ + +
`; } @@ -5191,6 +5278,14 @@ A+B document.getElementById("exportResultBtn")?.addEventListener("click", () => { if (lastResult) downloadText("nupack-design-result.json", JSON.stringify(lastResult, null, 2)); }); + document.getElementById("loadDesignedAnalysisBtn")?.addEventListener("click", () => { + try { + loadDesignedAnalysisInput(result); + } catch (error) { + setStatus(`${t("status_error")}: ${error.message}`, true); + } + }); + document.getElementById("runDesignedAnalysisBtn")?.addEventListener("click", runDesignedAnalysis); return; } result.complexes.forEach((complex, index) => { @@ -5302,6 +5397,55 @@ A+B } } + async function runDesignedAnalysis() { + if (activeSubmission) { + setStatus(formatStatus(t("submission_locked"))); + return; + } + let payload; + try { + payload = buildDesignedAnalysisPayload(lastResult); + const validationError = validatePayload(payload); + if (validationError) throw new Error(validationError); + } catch (error) { + setStatus(`${t("status_error")}: ${error.message}`, true); + return; + } + + setSubmissionLocked(true); + currentJobId = null; + setStatus(formatStatus(t("status_running"))); + results.innerHTML = ""; + + try { + refreshHealth(); + const { response, data } = await fetchJsonOrThrow("/api/jobs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!response.ok || data.status !== "accepted") { + throw new Error(data?.error || `Analysis failed with HTTP ${response.status}`); + } + currentJobId = data.job_id; + setStatus(formatStatus(t("status_waiting_job"))); + const result = await waitForJob(data.job_id); + result.job_id = data.job_id; + lastPayload = payload; + lastResult = result; + renderResult(result); + saveHistory(payload, result); + renderHistory(); + setStatus(formatStatus(t("status_done"))); + } catch (error) { + results.innerHTML = `

${t("request_failed")}

${String(error)}
`; + setStatus(t("status_error"), true); + refreshHealth(); + } finally { + setSubmissionLocked(false); + } + } + async function runAnalysis() { if (activeSubmission) { setStatus(formatStatus(t("submission_locked")));