diff --git a/system-block-ui/.env.example b/system-block-ui/.env.example new file mode 100644 index 0000000..f85c324 --- /dev/null +++ b/system-block-ui/.env.example @@ -0,0 +1 @@ +VITE_TI_RECOMMENDATIONS_URL=http://localhost:8787/api/ti-recommendations diff --git a/system-block-ui/README.md b/system-block-ui/README.md index ef9f296..31e48b9 100644 --- a/system-block-ui/README.md +++ b/system-block-ui/README.md @@ -29,6 +29,15 @@ bun run dev Open the local HTTP URL printed by Vite. Schematic evaluation and evaluated downloads are supported when the application is served over HTTP. +TI Support Intelligence recommendations are fetched from the cached Cloudflare +Worker at . No TI credentials are +stored in this application. To use a locally running proxy instead, copy +`.env.example` to `.env.local` before starting Vite. + +In the subcircuit picker, click a recommended TI portfolio part to reveal its +description. Recommendation descriptions stay collapsed until their part is +clicked. + To build the production application and serve that build locally: ```bash @@ -51,6 +60,10 @@ Use `npx vercel --prod` after checking the preview deployment. The configured install command performs a frozen install from `system-block-ui/bun.lock`; no dashboard build overrides are required. +The deployed cache proxy is used by default. Set +`VITE_TI_RECOMMENDATIONS_URL` only when a deployment should use another proxy +endpoint. + The remaining production checks are: ```bash diff --git a/system-block-ui/src/components/SubcircuitPickerModal.tsx b/system-block-ui/src/components/SubcircuitPickerModal.tsx index 8dca615..c775de6 100644 --- a/system-block-ui/src/components/SubcircuitPickerModal.tsx +++ b/system-block-ui/src/components/SubcircuitPickerModal.tsx @@ -1,6 +1,11 @@ -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import type { SubcircuitDefinition } from "../model"; +import { + getTiRecommendations, + matchTiRecommendedDefinitionIds, + type TiRecommendedPart, +} from "../ti-recommendations"; export function getSelectableSubcircuitCandidates( definitions: readonly SubcircuitDefinition[], @@ -11,6 +16,7 @@ export function getSelectableSubcircuitCandidates( (definition) => definition.id !== currentDefinition.id && definition.canInstantiate !== false && + definition.sourcePath.startsWith("lib/subcircuits/") && definition.category === currentDefinition.category, ) .sort((a, b) => a.title.localeCompare(b.title, "en")); @@ -29,10 +35,80 @@ export function SubcircuitPickerModal({ onClose, onSelect, }: SubcircuitPickerModalProps) { - const candidates = useMemo( - () => getSelectableSubcircuitCandidates(definitions, currentDefinition), - [currentDefinition, definitions], + const recommendationDefinitions = useMemo( + () => + definitions.filter( + (definition) => + definition.canInstantiate !== false && + definition.sourcePath.startsWith("lib/subcircuits/") && + definition.category === currentDefinition.category, + ), + [currentDefinition.category, definitions], ); + const candidates = useMemo(() => { + const selectable = getSelectableSubcircuitCandidates( + definitions, + currentDefinition, + ); + return [currentDefinition, ...selectable].sort((a, b) => + a.title.localeCompare(b.title, "en"), + ); + }, [currentDefinition, definitions]); + const [recommendedIds, setRecommendedIds] = useState>( + () => new Set(), + ); + const [recommendedParts, setRecommendedParts] = useState< + readonly TiRecommendedPart[] + >([]); + const [expandedPartNumber, setExpandedPartNumber] = useState( + null, + ); + const [isFetchingRecommendations, setIsFetchingRecommendations] = + useState(false); + const widerPortfolioParts = useMemo( + () => + recommendedParts.filter( + (part) => + matchTiRecommendedDefinitionIds( + [part.partNumber], + recommendationDefinitions, + ).size === 0, + ), + [recommendationDefinitions, recommendedParts], + ); + + useEffect(() => { + let active = true; + setRecommendedIds(new Set()); + setRecommendedParts([]); + setExpandedPartNumber(null); + if (candidates.length === 0) { + setIsFetchingRecommendations(false); + return; + } + setIsFetchingRecommendations(true); + void getTiRecommendations( + currentDefinition.category, + recommendationDefinitions, + ).then( + (recommendations) => { + if (!active) return; + setRecommendedIds(recommendations.definitionIds); + setRecommendedParts(recommendations.parts); + setIsFetchingRecommendations(false); + }, + () => { + if (active) setIsFetchingRecommendations(false); + }, + ); + return () => { + active = false; + }; + }, [ + candidates.length, + currentDefinition.category, + recommendationDefinitions, + ]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -70,25 +146,91 @@ export function SubcircuitPickerModal({
- Available parts - {candidates.length} + Available subcircuits +
+ {isFetchingRecommendations && ( + +