Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.
The real-group profiles stay as a test, so a change that moves a pick fails. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
96ca15d8e3
commit
bcbc6f30d9
89 files changed
+36555
-2828
No files matched your search
@@ -9,6 +9,12 @@ import {
|
||||
loadFacetScores,
|
||||
} from "../../../../lib/create/facetRecommendationsLoad";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
import { groupAnswersFromCreateFlowState } from "../../../../lib/create/groupAnswers";
|
||||
import {
|
||||
deckData,
|
||||
recommendDeck,
|
||||
} from "../../../../lib/create/deckRecommend";
|
||||
import type { GroupAnswers } from "../../../../lib/create/deckTypes";
|
||||
|
||||
/**
|
||||
* Card-deck section ids served by `/api/create-flow/methods` (CR-88 §9.2).
|
||||
@@ -125,63 +131,46 @@ export function useFacetRecommendations(
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable comparator for re-ranking a messages-file `methods[]` array. Higher
|
||||
* `scoresBySlug[id]` first; ties fall back to authoring index, so a
|
||||
* zero-facet user sees the original ordering verbatim.
|
||||
* One card per job, then the display rule for the top `limit` cards.
|
||||
* Cards marked every-rule carry no Recommended tag. With no facets, the
|
||||
* first `limit` cards in deck order show and nothing is tagged.
|
||||
*/
|
||||
export function rankMethodsByScore<T extends { id: string }>(
|
||||
methods: readonly T[],
|
||||
scoresBySlug: Record<string, number>,
|
||||
): T[] {
|
||||
const indexById = new Map<string, number>();
|
||||
methods.forEach((m, i) => indexById.set(m.id, i));
|
||||
return [...methods].sort((a, b) => {
|
||||
const sa = scoresBySlug[a.id] ?? 0;
|
||||
const sb = scoresBySlug[b.id] ?? 0;
|
||||
if (sa !== sb) return sb - sa;
|
||||
return (indexById.get(a.id) ?? 0) - (indexById.get(b.id) ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks (a) which method ids fill the compact card stack and (b) which of
|
||||
* those should render with the "Recommended" tag. The messages JSON no
|
||||
* longer carries a static `recommended` flag — both selections come
|
||||
* entirely from facet scores (CR-88 §10).
|
||||
*
|
||||
* Behavior:
|
||||
* - Facets selected & at least one method scored > 0 →
|
||||
* `compactCardIds` = up to `limit` top-scored methods (1..limit cards;
|
||||
* never padded with unrecommended fillers). All shown cards get the
|
||||
* "Recommended" badge.
|
||||
* - No facets selected, or every method scored 0 → `compactCardIds` =
|
||||
* first `limit` in ranked/authoring order, `recommendedIds` empty (no
|
||||
* badges shown — honest "no signal yet" fallback).
|
||||
*
|
||||
* `CardStack.view` is responsible for laying out variable-length compact
|
||||
* arrays gracefully (uses `.map`/`.slice` and length-guarded indexing).
|
||||
*/
|
||||
export function deriveCompactCards<T extends { id: string }>(
|
||||
rankedMethods: readonly T[],
|
||||
scoresBySlug: Record<string, number>,
|
||||
hasAnyFacets: boolean,
|
||||
export function deriveCompactCards(
|
||||
section: RecommendationSection,
|
||||
answers: GroupAnswers,
|
||||
limit: number,
|
||||
): { compactCardIds: string[]; recommendedIds: Set<string> } {
|
||||
const fallback = () => ({
|
||||
compactCardIds: rankedMethods.slice(0, limit).map((m) => m.id),
|
||||
recommendedIds: new Set<string>(),
|
||||
});
|
||||
|
||||
if (!hasAnyFacets) return fallback();
|
||||
|
||||
const matched = rankedMethods.filter(
|
||||
(m) => (scoresBySlug[m.id] ?? 0) > 0,
|
||||
);
|
||||
if (matched.length === 0) return fallback();
|
||||
|
||||
const top = matched.slice(0, limit);
|
||||
): {
|
||||
compactCardIds: string[];
|
||||
recommendedIds: Set<string>;
|
||||
orderedIds: string[];
|
||||
} {
|
||||
const recommendation = recommendDeck(deckData(section), answers, { limit });
|
||||
const orderedIds = recommendation.fallback
|
||||
? recommendation.seeAll
|
||||
: [
|
||||
...recommendation.shown.map((card) => card.id),
|
||||
...recommendation.seeAll,
|
||||
];
|
||||
if (recommendation.fallback) {
|
||||
return {
|
||||
compactCardIds: orderedIds.slice(0, limit),
|
||||
recommendedIds: new Set<string>(),
|
||||
orderedIds,
|
||||
};
|
||||
}
|
||||
return {
|
||||
compactCardIds: top.map((m) => m.id),
|
||||
recommendedIds: new Set(top.map((m) => m.id)),
|
||||
compactCardIds: recommendation.shown.map((card) => card.id),
|
||||
recommendedIds: new Set(
|
||||
recommendation.recommended
|
||||
.filter((card) => card.tag === "recommended")
|
||||
.map((card) => card.id),
|
||||
),
|
||||
orderedIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function groupAnswersForRecommendations(
|
||||
state: Parameters<typeof groupAnswersFromCreateFlowState>[0],
|
||||
): GroupAnswers {
|
||||
return groupAnswersFromCreateFlowState(state);
|
||||
}
|
||||
@@ -4,68 +4,80 @@ import { useMemo } from "react";
|
||||
import { mergeCompactCardIdsWithPinnedSelected } from "../../../../lib/create/methodCardDisplayOrder";
|
||||
import {
|
||||
deriveCompactCards,
|
||||
rankMethodsByScore,
|
||||
useFacetRecommendations,
|
||||
groupAnswersForRecommendations,
|
||||
type RecommendationSection,
|
||||
} from "./useFacetRecommendations";
|
||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||
|
||||
type MethodEntry = { id: string; label: string; supportText: string };
|
||||
|
||||
function orderByIds<T extends { id: string }>(
|
||||
methods: readonly T[],
|
||||
orderedIds: readonly string[],
|
||||
): T[] {
|
||||
const byId = new Map(methods.map((method) => [method.id, method]));
|
||||
const ordered = orderedIds.flatMap((id) => {
|
||||
const method = byId.get(id);
|
||||
return method ? [method] : [];
|
||||
});
|
||||
const seen = new Set(ordered.map((method) => method.id));
|
||||
return [...ordered, ...methods.filter((method) => !seen.has(method.id))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies score ranking and compact-slot rules. Expanded CardStack order stays
|
||||
* the ranked catalog (selected cards keep their place). Compact slots still
|
||||
* pin selected ids first so a pick outside the unpinned top-N remains visible
|
||||
* when the stack is collapsed.
|
||||
* Deck order and the compact stack come from the job rule. Current screens
|
||||
* show five cards; a deck screen passes seven. Selected cards still pin
|
||||
* into the compact stack.
|
||||
*/
|
||||
export function useMethodCardDeckOrdering(
|
||||
section: RecommendationSection,
|
||||
methods: readonly MethodEntry[],
|
||||
selectedIds: readonly string[],
|
||||
) {
|
||||
const { scoresBySlug, hasAnyFacets, isReady } =
|
||||
useFacetRecommendations(section);
|
||||
const recommendationsReady = !hasAnyFacets || isReady;
|
||||
const { state } = useCreateFlow();
|
||||
const answers = useMemo(
|
||||
() => groupAnswersForRecommendations(state),
|
||||
[state],
|
||||
);
|
||||
const deck = useMemo(
|
||||
() => deriveCompactCards(section, answers, 5),
|
||||
[section, answers],
|
||||
);
|
||||
|
||||
const rankedMethods = useMemo(
|
||||
() => rankMethodsByScore(methods, scoresBySlug),
|
||||
[methods, scoresBySlug],
|
||||
() => orderByIds(methods, deck.orderedIds),
|
||||
[methods, deck.orderedIds],
|
||||
);
|
||||
|
||||
const selectionShowcaseActive = selectedIds.length > 0;
|
||||
const displayMethods = rankedMethods;
|
||||
|
||||
const { compactCardIds: baseCompactCardIds, recommendedIds } = useMemo(
|
||||
() =>
|
||||
deriveCompactCards(
|
||||
rankedMethods,
|
||||
scoresBySlug,
|
||||
hasAnyFacets,
|
||||
/* limit */ 5,
|
||||
),
|
||||
[rankedMethods, scoresBySlug, hasAnyFacets],
|
||||
);
|
||||
const { recommendedIds } = deck;
|
||||
|
||||
const compactCardIds = useMemo(
|
||||
() =>
|
||||
mergeCompactCardIdsWithPinnedSelected(
|
||||
rankedMethods.map((m) => m.id),
|
||||
baseCompactCardIds,
|
||||
rankedMethods.map((method) => method.id),
|
||||
deck.compactCardIds,
|
||||
selectedIds,
|
||||
selectionShowcaseActive,
|
||||
5,
|
||||
),
|
||||
[rankedMethods, baseCompactCardIds, selectedIds, selectionShowcaseActive],
|
||||
[
|
||||
rankedMethods,
|
||||
deck.compactCardIds,
|
||||
selectedIds,
|
||||
selectionShowcaseActive,
|
||||
],
|
||||
);
|
||||
|
||||
const sampleCards = useMemo(
|
||||
() =>
|
||||
displayMethods.map((entry) => ({
|
||||
rankedMethods.map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
supportText: entry.supportText,
|
||||
recommended: recommendedIds.has(entry.id),
|
||||
})),
|
||||
[displayMethods, recommendedIds],
|
||||
[rankedMethods, recommendedIds],
|
||||
);
|
||||
|
||||
const methodById = useMemo(
|
||||
@@ -75,11 +87,11 @@ export function useMethodCardDeckOrdering(
|
||||
|
||||
return {
|
||||
rankedMethods,
|
||||
displayMethods,
|
||||
displayMethods: rankedMethods,
|
||||
compactCardIds,
|
||||
recommendedIds,
|
||||
sampleCards,
|
||||
methodById,
|
||||
recommendationsReady,
|
||||
recommendationsReady: true,
|
||||
};
|
||||
}
|
||||
@@ -113,7 +113,10 @@ export function useTemplateReviewActions({
|
||||
setTemplateReviewApplyError(loaded.message);
|
||||
return;
|
||||
}
|
||||
const prefill = buildTemplateCustomizePrefill(loaded.template.body);
|
||||
const prefill = buildTemplateCustomizePrefill(
|
||||
loaded.template.body,
|
||||
templateReviewSlug,
|
||||
);
|
||||
const pinPatch = methodSectionsPinsForHydratedSelections(prefill);
|
||||
const hasCommunityName =
|
||||
typeof state.title === "string" && state.title.trim().length > 0;
|
||||
@@ -182,7 +185,10 @@ export function useTemplateReviewActions({
|
||||
// (reset + update separately could leave selections undefined in Strict Mode).
|
||||
replaceState((prev) => {
|
||||
const base = stripCustomRuleSelectionFields(prev);
|
||||
const customizePrefill = buildTemplateCustomizePrefill(doc);
|
||||
const customizePrefill = buildTemplateCustomizePrefill(
|
||||
doc,
|
||||
templateReviewSlug ?? undefined,
|
||||
);
|
||||
const hasValuesSeed =
|
||||
customizePrefill.selectedCoreValueIds !== undefined;
|
||||
|
||||
|
||||
Reference in new issue
Block a user