Clarify training advisor table and create goal groups from + button.
Better column labels and legends reduce confusion between per-craft materials, level-up time, and craft count; the advisor + action now opens a new goal group with missing material targets plus the main activity goal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+123
-21
@@ -793,6 +793,15 @@ function formatAdvisorMissing(missing) {
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function advisorTh(labelKey, hintKey, className = "") {
|
||||
const label = t(labelKey);
|
||||
const hint = hintKey ? t(hintKey) : "";
|
||||
const cls = className ? ` class="${className}"` : "";
|
||||
return hint
|
||||
? `<th scope="col"${cls} title="${esc(hint)}">${esc(label)}</th>`
|
||||
: `<th scope="col"${cls}>${esc(label)}</th>`;
|
||||
}
|
||||
|
||||
function renderSkillAdvisorCard() {
|
||||
const hint = document.getElementById("skill-advisor-hint");
|
||||
const body = document.getElementById("skill-advisor-body");
|
||||
@@ -846,7 +855,8 @@ function renderSkillAdvisorCard() {
|
||||
const crafts = rec.crafts_to_next_level ?? 0;
|
||||
const goalLabel = t("skills.advisorAdoptGoalFor", { name: rec.display_name, count: fmt(crafts) });
|
||||
const goalCell = crafts > 0
|
||||
? `<button type="button" class="goal-add-btn skill-advisor-goal-btn" data-activity-key="${esc(rec.activity_key)}" data-crafts="${crafts}" title="${esc(goalLabel)}" aria-label="${esc(goalLabel)}">+</button>`
|
||||
? `<span class="skill-advisor-craft-qty" title="${esc(t("skills.advisorGoalColHint"))}">${esc(fmt(crafts))}×</span>
|
||||
<button type="button" class="goal-add-btn skill-advisor-goal-btn" data-activity-key="${esc(rec.activity_key)}" data-crafts="${crafts}" title="${esc(goalLabel)}" aria-label="${esc(goalLabel)}">+</button>`
|
||||
: `<span class="inv-spark-empty">—</span>`;
|
||||
return `<tr>
|
||||
<td>${esc(rec.display_name)}</td>
|
||||
@@ -854,7 +864,7 @@ function renderSkillAdvisorCard() {
|
||||
<td>${rec.level_required}</td>
|
||||
<td class="skill-advisor-mats">${esc(mats)}</td>
|
||||
<td class="num">${esc(eta)}</td>
|
||||
<td class="col-actions">
|
||||
<td class="col-actions skill-advisor-goal-cell">
|
||||
${goalCell}
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -869,16 +879,17 @@ function renderSkillAdvisorCard() {
|
||||
<div class="table-wrap">
|
||||
<table class="skill-advisor-table">
|
||||
<thead><tr>
|
||||
<th>${esc(t("skills.advisorActivity"))}</th>
|
||||
<th>${esc(t("skills.advisorXpMin"))}</th>
|
||||
<th>${esc(t("skills.advisorReqLevel"))}</th>
|
||||
<th>${esc(t("skills.advisorMaterials"))}</th>
|
||||
<th>${esc(t("skills.advisorEtaCol"))}</th>
|
||||
<th class="col-actions">${esc(t("goals.actions"))}</th>
|
||||
${advisorTh("skills.advisorActivity")}
|
||||
${advisorTh("skills.advisorXpMin")}
|
||||
${advisorTh("skills.advisorReqLevel")}
|
||||
${advisorTh("skills.advisorMaterials", "skills.advisorMaterialsHint")}
|
||||
${advisorTh("skills.advisorEtaCol", "skills.advisorEtaColHint")}
|
||||
${advisorTh("skills.advisorGoalCol", "skills.advisorGoalColHint", "col-actions")}
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
</div>
|
||||
<p class="skill-advisor-legend">${esc(t("skills.advisorTableLegend"))}</p>`;
|
||||
|
||||
body.querySelector(".skill-advisor-skill-goal-btn")?.addEventListener("click", () => {
|
||||
adoptAdvisorSkillGoal();
|
||||
@@ -915,30 +926,121 @@ async function adoptAdvisorSkillGoal() {
|
||||
alert(t("skills.advisorGoalCreated", { name: adv.skill_name, target: targetLevel }));
|
||||
}
|
||||
|
||||
function inventoryQtyMap(data) {
|
||||
const map = {};
|
||||
for (const item of data?.inventory || []) {
|
||||
map[item.key] = item.qty;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function collectExistingGoalItemKeys() {
|
||||
const keys = new Set();
|
||||
const data = state.goals.data || {};
|
||||
for (const goal of data.ungrouped || []) {
|
||||
if (goal.goal_type === "item") keys.add(goal.item_key);
|
||||
}
|
||||
for (const group of data.groups || []) {
|
||||
for (const goal of group.goals || []) {
|
||||
if (goal.goal_type === "item") keys.add(goal.item_key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function advisorMaterialGoalTargets(rec, crafts, inventoryMap) {
|
||||
const targets = [];
|
||||
for (const [key, perCraft] of Object.entries(rec.materials || {})) {
|
||||
const totalNeeded = Number(perCraft) * crafts;
|
||||
const have = inventoryMap[key] ?? 0;
|
||||
const missing = totalNeeded - have;
|
||||
if (missing > 0) {
|
||||
targets.push({ item_key: key, target_qty: missing });
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
async function createGoalInGroup(itemKey, targetQty, groupId, existingKeys) {
|
||||
if (existingKeys.has(itemKey)) {
|
||||
return { ok: false, skipped: true };
|
||||
}
|
||||
const res = await fetch(`${apiBase()}/goals`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
item_key: itemKey,
|
||||
target_qty: targetQty,
|
||||
mode: "relative",
|
||||
group_id: groupId,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return { ok: false, skipped: true };
|
||||
}
|
||||
existingKeys.add(itemKey);
|
||||
return { ok: true, goal: await res.json() };
|
||||
}
|
||||
|
||||
async function adoptAdvisorItemGoal(activityKey, crafts) {
|
||||
const adv = state.skills.advisor;
|
||||
if (!adv?.supported || !activityKey) return;
|
||||
const rec = adv.recommendations?.find((r) => r.activity_key === activityKey);
|
||||
const name = rec?.display_name || activityKey.replace(/_/g, " ");
|
||||
const count = Number(crafts);
|
||||
if (!count || count <= 0) return;
|
||||
const res = await fetch(`${apiBase()}/goals`, {
|
||||
if (!count || count <= 0 || !rec) return;
|
||||
|
||||
await loadGoals();
|
||||
const existingKeys = collectExistingGoalItemKeys();
|
||||
const inventoryMap = inventoryQtyMap(state.data);
|
||||
const materialTargets = advisorMaterialGoalTargets(rec, count, inventoryMap);
|
||||
|
||||
const groupName = t("skills.advisorGoalGroupName", { name });
|
||||
const groupRes = await fetch(`${apiBase()}/goal-groups`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
item_key: activityKey,
|
||||
target_qty: count,
|
||||
mode: "relative",
|
||||
}),
|
||||
body: JSON.stringify({ name: groupName }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
alert(result.error || t("goals.createFailed"));
|
||||
const groupData = await groupRes.json();
|
||||
if (!groupRes.ok) {
|
||||
alert(groupData.error || t("goals.groupCreateFailed"));
|
||||
return;
|
||||
}
|
||||
trackEvent("Goal Create", { source: "advisor", type: "item", mode: "relative", group: "none" });
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const mat of materialTargets) {
|
||||
const result = await createGoalInGroup(mat.item_key, mat.target_qty, groupData.id, existingKeys);
|
||||
if (result.ok) created += 1;
|
||||
else skipped += 1;
|
||||
}
|
||||
|
||||
const mainResult = await createGoalInGroup(activityKey, count, groupData.id, existingKeys);
|
||||
if (mainResult.ok) created += 1;
|
||||
else skipped += 1;
|
||||
|
||||
if (created === 0) {
|
||||
await fetch(`${apiBase()}/goal-groups/${groupData.id}`, { method: "DELETE" });
|
||||
alert(t("skills.advisorItemGoalsNone"));
|
||||
return;
|
||||
}
|
||||
|
||||
trackEvent("Goal Group Create", { source: "advisor" });
|
||||
trackEvent("Goal Create", {
|
||||
source: "advisor",
|
||||
type: "item",
|
||||
mode: "relative",
|
||||
group: "new",
|
||||
materials: materialTargets.length,
|
||||
});
|
||||
await refreshGoalsAfterCreate();
|
||||
alert(t("skills.advisorItemGoalCreated", { name, count: fmt(count) }));
|
||||
|
||||
let message = t("skills.advisorItemGoalsCreated", { group: groupName, count: fmt(created) });
|
||||
if (skipped > 0) {
|
||||
message += ` ${t("skills.advisorItemGoalsSkipped", { count: fmt(skipped) })}`;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
|
||||
async function refreshGoalsAfterCreate() {
|
||||
|
||||
+13
-4
@@ -133,14 +133,23 @@
|
||||
"advisorActivity": "Aktivität",
|
||||
"advisorXpMin": "XP / Min.",
|
||||
"advisorReqLevel": "Level",
|
||||
"advisorMaterials": "Material",
|
||||
"advisorEtaCol": "Bis Level",
|
||||
"advisorMaterials": "Material (1×)",
|
||||
"advisorMaterialsHint": "Materialbedarf für einen einzelnen Craft — nicht für das gesamte Level-Up.",
|
||||
"advisorEtaCol": "Zeit (~Min.)",
|
||||
"advisorEtaColHint": "Geschätzte Spielzeit bis zum nächsten Level, wenn du nur diese Aktivität machst.",
|
||||
"advisorGoalCol": "Ziel (Anz.)",
|
||||
"advisorGoalColHint": "Anzahl Crafts bis zum nächsten Level. Mit + Zielgruppe inkl. fehlender Materialien anlegen.",
|
||||
"advisorTableLegend": "Material pro Craft; Zeit und Anzahl beziehen sich auf das komplette Level-Up. Bei 60 Sek. pro Craft können Minuten und Anzahl übereinstimmen.",
|
||||
"advisorEta": "~{minutes} Min.",
|
||||
"advisorMissing": "Fehlt: {items}",
|
||||
"advisorAdoptSkillGoal": "Level {level} als Ziel",
|
||||
"advisorAdoptGoalFor": "Ziel: {count}× {name} craften (bis nächstes Level)",
|
||||
"advisorAdoptGoalFor": "Zielgruppe anlegen: {count}× {name} craften plus fehlende Materialien",
|
||||
"advisorGoalGroupName": "{name} — Level-Up",
|
||||
"advisorGoalCreated": "Skill-Ziel angelegt: {name} → Level {target}",
|
||||
"advisorItemGoalCreated": "Item-Ziel angelegt: {count}× {name} craften"
|
||||
"advisorItemGoalCreated": "Item-Ziel angelegt: {count}× {name} craften",
|
||||
"advisorItemGoalsCreated": "Gruppe \"{group}\" angelegt mit {count} Zielen.",
|
||||
"advisorItemGoalsSkipped": "{count} Ziele übersprungen (bereits vorhanden).",
|
||||
"advisorItemGoalsNone": "Keine neuen Ziele angelegt — alle Items haben bereits ein Ziel."
|
||||
},
|
||||
"inventory": {
|
||||
"search": "Item suchen…",
|
||||
|
||||
+13
-4
@@ -133,14 +133,23 @@
|
||||
"advisorActivity": "Activity",
|
||||
"advisorXpMin": "XP / min",
|
||||
"advisorReqLevel": "Req. level",
|
||||
"advisorMaterials": "Materials",
|
||||
"advisorEtaCol": "Est. to level",
|
||||
"advisorMaterials": "Materials (×1)",
|
||||
"advisorMaterialsHint": "Materials needed for a single craft — not for the full level-up.",
|
||||
"advisorEtaCol": "Time (~min)",
|
||||
"advisorEtaColHint": "Estimated play time to the next level if you only use this activity.",
|
||||
"advisorGoalCol": "Goal (qty)",
|
||||
"advisorGoalColHint": "Number of crafts to reach the next level. Use + to create a goal group including missing materials.",
|
||||
"advisorTableLegend": "Materials are per craft; time and quantity refer to the full level-up. At 60 seconds per craft, minutes and quantity may match.",
|
||||
"advisorEta": "~{minutes} min",
|
||||
"advisorMissing": "Missing: {items}",
|
||||
"advisorAdoptSkillGoal": "Level {level} as goal",
|
||||
"advisorAdoptGoalFor": "Goal: craft {count}× {name} (to next level)",
|
||||
"advisorAdoptGoalFor": "Create goal group: craft {count}× {name} plus missing materials",
|
||||
"advisorGoalGroupName": "{name} — level-up",
|
||||
"advisorGoalCreated": "Skill goal created: {name} → level {target}",
|
||||
"advisorItemGoalCreated": "Item goal created: craft {count}× {name}"
|
||||
"advisorItemGoalCreated": "Item goal created: craft {count}× {name}",
|
||||
"advisorItemGoalsCreated": "Created group \"{group}\" with {count} goals.",
|
||||
"advisorItemGoalsSkipped": "{count} goals skipped (already exist).",
|
||||
"advisorItemGoalsNone": "No new goals created — all items already have a goal."
|
||||
},
|
||||
"inventory": {
|
||||
"search": "Search items…",
|
||||
|
||||
@@ -393,6 +393,27 @@ tr:hover td { background: var(--bg-hover); }
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.skill-advisor-legend {
|
||||
margin: 10px 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.skill-advisor-goal-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.skill-advisor-craft-qty {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.skill-advisor-summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user