"use client"; import { useEffect, useMemo, useState } from "react"; import { buildFacetQueryString } from "../../../../lib/create/buildFacetQueryString"; import type { MethodFacetApiSectionId } from "../../../../lib/create/customRuleFacets"; import { buildFacetRecommendationRequestKey, getCachedFacetScores, 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). * Same tuple as {@link METHOD_FACET_API_SECTION_IDS} (`CUSTOM_RULE_FACETS`, CR-92). */ export type RecommendationSection = MethodFacetApiSectionId; export type FacetRecommendationsResult = { /** `true` once the network call completes (or short-circuits with no facets). */ isReady: boolean; /** `slug → score`; missing slug means `0`. */ scoresBySlug: Record; /** * `true` iff the user has selected at least one community facet. When * `false`, callers should preserve authoring order rather than reranking. */ hasAnyFacets: boolean; }; const EMPTY_SCORES: Record = {}; function initialFacetRecommendationsResult( section: RecommendationSection, queryString: string, ): FacetRecommendationsResult { const hasAnyFacets = queryString.length > 0; if (!hasAnyFacets) { return { isReady: true, scoresBySlug: EMPTY_SCORES, hasAnyFacets: false, }; } const requestKey = buildFacetRecommendationRequestKey(section, queryString); const cached = getCachedFacetScores(requestKey); if (cached) { return { isReady: true, scoresBySlug: cached, hasAnyFacets: true, }; } return { isReady: false, scoresBySlug: EMPTY_SCORES, hasAnyFacets: true, }; } /** * Calls `GET /api/create-flow/methods?section=
&facet.*=...` for the * card-deck step `section` and returns a `slug → score` map for re-ranking * the messages-file `methods[]` array (CR-88 §10). * * Returns `{ isReady: true, scoresBySlug: {} }` when the user has not selected * any community facets — callers fall back to the authoring order. * * Network failures resolve to `scoresBySlug: {}` so the wizard is never * blocked on the recommendation backend. */ export function useFacetRecommendations( section: RecommendationSection, ): FacetRecommendationsResult { const { state } = useCreateFlow(); const queryString = useMemo( () => buildFacetQueryString(state), [state], ); const hasAnyFacets = queryString.length > 0; const [result, setResult] = useState(() => initialFacetRecommendationsResult(section, queryString), ); useEffect(() => { if (!hasAnyFacets) { setResult({ isReady: true, scoresBySlug: EMPTY_SCORES, hasAnyFacets: false, }); return; } const requestKey = buildFacetRecommendationRequestKey(section, queryString); const cached = getCachedFacetScores(requestKey); if (cached) { setResult({ isReady: true, scoresBySlug: cached, hasAnyFacets: true, }); return; } let cancelled = false; setResult((prev) => prev.isReady && prev.hasAnyFacets ? { ...prev, isReady: false } : { isReady: false, scoresBySlug: EMPTY_SCORES, hasAnyFacets: true }, ); void loadFacetScores(section, queryString).then((scoresBySlug) => { if (cancelled) return; setResult({ isReady: true, scoresBySlug, hasAnyFacets: true }); }); return () => { cancelled = true; }; }, [section, queryString, hasAnyFacets]); return result; } /** * 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 deriveCompactCards( section: RecommendationSection, answers: GroupAnswers, limit: number, ): { compactCardIds: string[]; recommendedIds: Set; 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(), orderedIds, }; } return { 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[0], ): GroupAnswers { return groupAnswersFromCreateFlowState(state); }