Skip to content

Commit 28e9bbe

Browse files
RyanHanszRyan Hanszclaude
authored
feat: Add Edit Search panel with improved labels (#167)
Co-authored-by: Ryan Hansz <ryan@Ryans-MacBook-Pro-2.local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0b40368 commit 28e9bbe

8 files changed

Lines changed: 851 additions & 49 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ app/sentence_transformers/
2525
# Playwright MCP screenshots
2626
.playwright-mcp/
2727

28+
# PR documentation screenshots
29+
docs/screenshots/
30+
2831
# Editor configs
2932
docs/.obsidian/
3033

frontend/src/app/[locale]/generate-referrals/page.tsx

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ import { ShareButtons } from "@/components/ShareButtons";
2727
import WelcomeUserInputScreen from "@/components/WelcomeUserInputScreen";
2828
import { PilotFeedbackBanner } from "@/components/PilotFeedbackBanner";
2929
import { GoodwillReferralToolHeaderPilot } from "@/components/GoodwillReferralToolHeaderPilot";
30-
import ClientDetailsPromptBubble from "@/components/ClientDetailsPromptBubble";
3130
import {
3231
ClientDetailsInput,
3332
resourceCategories,
3433
} from "@/components/ClientDetailsInput";
34+
import { RefinePromptPanel } from "@/components/RefinePromptPanel";
3535
import { RemoveResourceNotification } from "@/components/RemoveResourceNotification";
3636

3737
export default function Page() {
@@ -64,8 +64,6 @@ export default function Page() {
6464
const [errorMessage, setErrorMessage] = useState<string | undefined>(
6565
undefined,
6666
);
67-
const [requestAfterZipResolution, setRequestAfterZipResolution] =
68-
useState("");
6967

7068
const searchParams = useSearchParams();
7169

@@ -119,7 +117,12 @@ export default function Page() {
119117
setShowResultsView(true);
120118
};
121119

122-
async function findResources() {
120+
async function findResources(overrides?: {
121+
clientDescription?: string;
122+
locationText?: string;
123+
selectedCategories?: string[];
124+
selectedResourceTypes?: string[];
125+
}) {
123126
const prompt_version_id = searchParams?.get("prompt_version_id") ?? null;
124127
const suffix = searchParams?.get("suffix") ?? undefined;
125128

@@ -142,8 +145,7 @@ export default function Page() {
142145
}, 12000);
143146

144147
try {
145-
const request = await buildRequestWithResolvedZipCodes();
146-
setRequestAfterZipResolution(request);
148+
const request = await buildRequestWithResolvedZipCodes(overrides);
147149

148150
const {
149151
resources: finalResources,
@@ -244,7 +246,6 @@ export default function Page() {
244246
setIsStreamingActionPlan(false);
245247
setIsGeneratingActionPlan(false);
246248
setErrorMessage(undefined);
247-
setRequestAfterZipResolution("");
248249
setRecentlyRemoved(null);
249250
setRemovedResourceIndex(null);
250251
}
@@ -268,6 +269,34 @@ export default function Page() {
268269
}
269270
}
270271

272+
function handleRefineSearch(
273+
newClientDescription: string,
274+
newLocationText: string,
275+
newSelectedCategories: string[],
276+
newSelectedResourceTypes: string[],
277+
) {
278+
// Update state with new values
279+
setClientDescription(newClientDescription);
280+
setLocationText(newLocationText);
281+
setSelectedCategories(newSelectedCategories);
282+
setSelectedResourceTypes(newSelectedResourceTypes);
283+
284+
// Clear previous results and action plan
285+
setRetainedResources(undefined);
286+
setSelectedResources([]);
287+
setActionPlan(null);
288+
setStreamingPlan(null);
289+
setActionPlanResultId("");
290+
291+
// Trigger new search with overrides (since state updates are async)
292+
void findResources({
293+
clientDescription: newClientDescription,
294+
locationText: newLocationText,
295+
selectedCategories: newSelectedCategories,
296+
selectedResourceTypes: newSelectedResourceTypes,
297+
});
298+
}
299+
271300
async function generateActionPlan() {
272301
if (selectedResources.length === 0) return;
273302

@@ -338,7 +367,21 @@ export default function Page() {
338367
}
339368
}
340369

341-
const buildRequestWithResolvedZipCodes = async (): Promise<string> => {
370+
const buildRequestWithResolvedZipCodes = async (overrides?: {
371+
clientDescription?: string;
372+
locationText?: string;
373+
selectedCategories?: string[];
374+
selectedResourceTypes?: string[];
375+
}): Promise<string> => {
376+
// Use overrides if provided, otherwise use state values
377+
const effectiveClientDescription =
378+
overrides?.clientDescription ?? clientDescription;
379+
const effectiveLocationText = overrides?.locationText ?? locationText;
380+
const effectiveSelectedCategories =
381+
overrides?.selectedCategories ?? selectedCategories;
382+
const effectiveSelectedResourceTypes =
383+
overrides?.selectedResourceTypes ?? selectedResourceTypes;
384+
342385
// Helper function to replace zip codes with "city, state zip_code" format
343386
const replaceZipCodes = async (text: string): Promise<string> => {
344387
if (!text) return text;
@@ -378,10 +421,12 @@ export default function Page() {
378421
};
379422

380423
// Process both locationText and clientDescription for zip codes
381-
const processedLocationText = await replaceZipCodes(locationText);
382-
const processedClientDescription = await replaceZipCodes(clientDescription);
424+
const processedLocationText = await replaceZipCodes(effectiveLocationText);
425+
const processedClientDescription = await replaceZipCodes(
426+
effectiveClientDescription,
427+
);
383428

384-
const resourceTypeFilters = selectedCategories
429+
const resourceTypeFilters = effectiveSelectedCategories
385430
.map((categoryId) => {
386431
const category = resourceCategories.find((c) => c.id === categoryId);
387432
return category?.label;
@@ -394,9 +439,9 @@ export default function Page() {
394439
? "\nInclude resources that support the following categories: " +
395440
resourceTypeFilters
396441
: "") +
397-
(selectedResourceTypes.length > 0
442+
(effectiveSelectedResourceTypes.length > 0
398443
? "\nInclude the following types of providers: " +
399-
selectedResourceTypes.join(", ")
444+
effectiveSelectedResourceTypes.join(", ")
400445
: "") +
401446
(processedLocationText.length > 0
402447
? "\nFocus on resources close to the following location: " +
@@ -510,7 +555,7 @@ export default function Page() {
510555
data-testid="returnToSearchButton"
511556
>
512557
<ChevronLeft className="w-4 h-4" />
513-
Return To Search
558+
Start a new search
514559
</Button>
515560
<ShareButtons
516561
onPrint={handlePrint}
@@ -522,21 +567,36 @@ export default function Page() {
522567
</div>
523568
)}
524569

525-
{requestAfterZipResolution && (
526-
<ClientDetailsPromptBubble
527-
clientDescription={requestAfterZipResolution}
570+
{(isStreamingResources || showResultsView) && (
571+
<RefinePromptPanel
572+
clientDescription={clientDescription}
573+
locationText={locationText}
574+
selectedCategories={selectedCategories}
575+
selectedResourceTypes={selectedResourceTypes}
576+
isLoading={loading}
577+
isStreamingResources={isStreamingResources}
578+
onRefine={handleRefineSearch}
528579
/>
529580
)}
530581

531582
{(isStreamingResources || showResultsView) && (
532-
<ResourcesList
533-
resources={
534-
(streamingResources as Resource[]) ?? retainedResources ?? []
535-
}
536-
errorMessage={errorMessage}
537-
handleRemoveResource={handleRemoveResource}
538-
isSearching={isStreamingResources && !hasReceivedFirstResource}
539-
/>
583+
<>
584+
<h2 className="text-lg font-semibold text-gray-900 mt-4">
585+
Resources
586+
</h2>
587+
<ResourcesList
588+
resources={
589+
(streamingResources as Resource[]) ??
590+
retainedResources ??
591+
[]
592+
}
593+
errorMessage={errorMessage}
594+
handleRemoveResource={handleRemoveResource}
595+
isSearching={
596+
isStreamingResources && !hasReceivedFirstResource
597+
}
598+
/>
599+
</>
540600
)}
541601

542602
{showResultsView &&
@@ -584,6 +644,9 @@ export default function Page() {
584644
resources={retainedResources ?? []}
585645
clientDescription={clientDescription}
586646
actionPlan={actionPlan}
647+
selectedCategories={selectedCategories}
648+
locationText={locationText}
649+
selectedResourceTypes={selectedResourceTypes}
587650
/>
588651
</div>
589652
</>

frontend/src/components/ClientDetailsInput.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const resourceCategories = [
2828
];
2929

3030
// Provider types for the Resource Provider Selection section
31-
const providerTypes: {
31+
export const providerTypes: {
3232
id: string;
3333
label: string;
3434
emoji?: string;
@@ -90,13 +90,14 @@ export function ClientDetailsInput({
9090
data-testid="clientDescriptionInputSection"
9191
>
9292
<CardContent className="p-4">
93-
<label
94-
htmlFor="clientDescriptionInput"
95-
className="font-medium text-gray-900 mb-3 flex items-center gap-2"
96-
>
93+
<h2 className="font-medium text-gray-900 mb-1 flex items-center gap-2 text-base">
9794
<UserRound className="w-4 h-4 text-blue-600" aria-hidden="true" />
98-
Tell us about your client
99-
</label>
95+
What kind of resources is your client looking for?
96+
</h2>
97+
<p className="text-sm text-gray-500 mb-3">
98+
Include details about your client&apos;s circumstances to help find
99+
better resources
100+
</p>
100101
<Textarea
101102
placeholder="Example: My client is looking for medical assistant training, diapers and clothes for job interviews."
102103
id="clientDescriptionInput"
Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,64 @@
1+
import {
2+
resourceCategories,
3+
providerTypes,
4+
} from "@/components/ClientDetailsInput";
5+
6+
interface ClientDetailsPromptBubbleProps {
7+
clientDescription: string;
8+
selectedCategories?: string[];
9+
locationText?: string;
10+
selectedResourceTypes?: string[];
11+
}
12+
113
const ClientDetailsPromptBubble = ({
214
clientDescription,
3-
}: {
4-
clientDescription: string;
5-
}) => {
15+
selectedCategories = [],
16+
locationText = "",
17+
selectedResourceTypes = [],
18+
}: ClientDetailsPromptBubbleProps) => {
19+
// Get category labels from IDs
20+
const categoryLabels = selectedCategories
21+
.map((id) => resourceCategories.find((c) => c.id === id)?.label)
22+
.filter(Boolean);
23+
24+
// Get provider type labels from IDs
25+
const providerLabels = selectedResourceTypes
26+
.map((id) => providerTypes.find((p) => p.id === id)?.label)
27+
.filter(Boolean);
28+
29+
// Build metadata parts
30+
const metadataParts: string[] = [];
31+
if (categoryLabels.length > 0) {
32+
metadataParts.push(`Categories: ${categoryLabels.join(", ")}`);
33+
}
34+
if (locationText) {
35+
metadataParts.push(`Location: ${locationText}`);
36+
}
37+
if (providerLabels.length > 0) {
38+
metadataParts.push(`Providers: ${providerLabels.join(", ")}`);
39+
}
40+
641
return (
7-
<div className="bg-gray-100 rounded-2xl p-4 border">
8-
<p className="text-lg font-semibold text-gray-900 text-center mb-3">
9-
{clientDescription.split("\n").map((line, index, array) => (
10-
<span key={index}>
11-
{line}
12-
{index < array.length - 1 && <br />}
13-
</span>
14-
))}
15-
</p>
42+
<div data-testid="yourSearchSection">
43+
<h2 className="text-lg font-semibold text-gray-900 mb-3">Your Search</h2>
44+
<div className="bg-gray-100 rounded-2xl p-4 border">
45+
<p
46+
className="text-lg font-semibold text-gray-900 text-center"
47+
data-testid="searchQueryDisplay"
48+
>
49+
{clientDescription}
50+
</p>
51+
{metadataParts.length > 0 && (
52+
<p
53+
className="text-sm text-gray-600 text-center mt-2"
54+
data-testid="searchMetadataDisplay"
55+
>
56+
{metadataParts.join(" | ")}
57+
</p>
58+
)}
59+
</div>
1660
</div>
1761
);
1862
};
63+
1964
export default ClientDetailsPromptBubble;

0 commit comments

Comments
 (0)