Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | 3x 3x 22x 22x 22x 22x 22x 22x 1x 22x 2x 2x 22x 4x 4x 4x 22x 3x 3x 3x 22x 6x 6x 6x 3x 3x 1x 2x 2x 22x 1x 1x 22x | /**
* useTopicSelection (Epic 14, Story 14.F.3)
*
* The select-a-topic-for-an-event decision logic, lifted out of TopicDetailsPanel
* so the focused overlay's card grid can reuse it once (no per-card duplication).
*
* Routing mirrors TopicDetailsPanel exactly:
* - stalenessScore < 50 (too recent) → 'override' (justification required)
* - else high similarity (>0.7) → 'similar' (confirm, justification optional)
* - else → commit directly
*
* On a successful select it calls `onConfirmed(topicCode)` (the overlay then pins
* the topic and switches to brainstorming). Recompose, not rewrite (NFR9) — no
* new backend; reuses `useSelectTopicForEvent` + `useSimilarTopics`.
*/
import { useState } from 'react';
import { useSelectTopicForEvent, useSimilarTopics } from '@/hooks/useTopics';
import type { Topic } from '@/types/topic.types';
export const SIMILARITY_THRESHOLD = 0.7;
export const RECENT_STALENESS = 50;
export type TopicSelectionMode = 'idle' | 'similar' | 'override';
export interface UseTopicSelectionOptions {
eventCode: string;
onConfirmed?: (topicCode: string) => void;
}
export interface UseTopicSelectionResult {
/** The topic currently going through a confirm dialog (null when none). */
actingTopic: Topic | null;
/** Which confirm dialog (if any) is open. */
mode: TopicSelectionMode;
justification: string;
setJustification: (value: string) => void;
isPending: boolean;
/** High-similarity (>0.7) topics for the acting topic — drives the similar dialog list. */
similarTopics: Topic[];
/** Does this topic have any >0.7 similarity score? (drives the inline card warning) */
hasHighSimilarity: (topic: Topic) => boolean;
/** Entry point from a card's Select button — routes to direct commit / similar / override. */
requestSelect: (topic: Topic) => void;
/** Confirm the acting topic from inside a dialog. */
commit: () => void;
/** Dismiss any open dialog without selecting. */
cancel: () => void;
}
export function useTopicSelection({
eventCode,
onConfirmed,
}: UseTopicSelectionOptions): UseTopicSelectionResult {
const [actingTopic, setActingTopic] = useState<Topic | null>(null);
const [mode, setMode] = useState<TopicSelectionMode>('idle');
const [justification, setJustification] = useState('');
const selectMutation = useSelectTopicForEvent();
// Only fetches when a topic is acting (id empty → query disabled).
const { data: similarTopicsRaw } = useSimilarTopics(actingTopic?.topicCode ?? '');
const hasHighSimilarity = (topic: Topic): boolean =>
(topic.similarityScores ?? []).some((s) => (s.score ?? 0) > SIMILARITY_THRESHOLD);
const similarTopics: Topic[] = (similarTopicsRaw ?? []).filter((st) => {
const sim = (actingTopic?.similarityScores ?? []).find((s) => s.topicCode === st.topicCode);
return !!sim && (sim.score ?? 0) > SIMILARITY_THRESHOLD;
});
const reset = () => {
setActingTopic(null);
setMode('idle');
setJustification('');
};
const doCommit = (topic: Topic, just?: string) => {
selectMutation.mutate(
{ eventCode, request: { topicCode: topic.topicCode, justification: just || undefined } },
{
onSuccess: () => {
onConfirmed?.(topic.topicCode);
reset();
},
}
);
};
const requestSelect = (topic: Topic) => {
setActingTopic(topic);
setJustification('');
if (topic.stalenessScore < RECENT_STALENESS) {
setMode('override');
} else if (hasHighSimilarity(topic)) {
setMode('similar');
} else {
setMode('idle');
doCommit(topic);
}
};
const commit = () => {
Eif (actingTopic) {
doCommit(actingTopic, justification);
}
};
return {
actingTopic,
mode,
justification,
setJustification,
isPending: selectMutation.isPending,
similarTopics,
hasHighSimilarity,
requestSelect,
commit,
cancel: reset,
};
}
export default useTopicSelection;
|