Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions system-block-ui/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_TI_RECOMMENDATIONS_URL=http://localhost:8787/api/ti-recommendations
13 changes: 13 additions & 0 deletions system-block-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://ti-mcp-cache-proxy.seve.workers.dev>. 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
Expand All @@ -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
Expand Down
180 changes: 161 additions & 19 deletions system-block-ui/src/components/SubcircuitPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -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[],
Expand All @@ -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"));
Expand All @@ -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<ReadonlySet<string>>(
() => new Set(),
);
const [recommendedParts, setRecommendedParts] = useState<
readonly TiRecommendedPart[]
>([]);
const [expandedPartNumber, setExpandedPartNumber] = useState<string | null>(
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) => {
Expand Down Expand Up @@ -70,25 +146,91 @@ export function SubcircuitPickerModal({

<div className="subcircuit-picker-catalog">
<div className="subcircuit-picker-results-heading">
<span>Available parts</span>
<small>{candidates.length}</small>
<span>Available subcircuits</span>
<div className="subcircuit-picker-results-summary">
{isFetchingRecommendations && (
<span
aria-live="polite"
className="ti-recommendation-fetching"
role="status"
>
<i aria-hidden="true" />
Fetching TI recommendations…
</span>
)}
<small>{candidates.length}</small>
</div>
</div>

<div className="subcircuit-picker-results">
{candidates.map((definition) => (
<button
aria-label={`Select ${definition.title}`}
className="subcircuit-candidate"
key={definition.id}
onClick={() => onSelect(definition)}
type="button"
{widerPortfolioParts.length > 0 && (
<section
aria-label="Recommendations from the wider TI portfolio"
className="ti-portfolio-recommendations"
>
<strong>{definition.title}</strong>
{definition.description && (
<span>{definition.description}</span>
)}
</button>
))}
<div className="ti-portfolio-recommendations-heading">
<strong>TI portfolio recommendations</strong>
<small>{widerPortfolioParts.length}</small>
</div>
{widerPortfolioParts.map((part) => {
const isExpanded = expandedPartNumber === part.partNumber;
return (
<button
aria-expanded={isExpanded}
className="ti-portfolio-part"
key={part.partNumber}
onClick={() =>
setExpandedPartNumber(
isExpanded ? null : part.partNumber,
)
}
type="button"
>
<div>
<strong>{part.name}</strong>
<small>Recommended</small>
</div>
{isExpanded && part.description && (
<span>{part.description}</span>
)}
</button>
);
})}
</section>
)}

{candidates.map((definition) => {
const isCurrent = definition.id === currentDefinition.id;
return (
<button
aria-label={
isCurrent
? `Current subcircuit: ${definition.title}`
: `Select ${definition.title}`
}
className="subcircuit-candidate"
disabled={isCurrent}
key={definition.id}
onClick={() => onSelect(definition)}
type="button"
>
<div className="subcircuit-candidate-heading">
<strong>{definition.title}</strong>
<div className="subcircuit-candidate-badges">
{isCurrent && <small data-tone="current">Current</small>}
{recommendedIds.has(definition.id) && (
<small title="Recommended by TI Support Intelligence">
Recommended
</small>
)}
</div>
</div>
{definition.description && (
<span>{definition.description}</span>
)}
</button>
);
})}

{candidates.length === 0 && (
<div className="subcircuit-picker-empty">
Expand Down
Loading
Loading