Skip to content

Commit 1d2f1ab

Browse files
CHORE: moved common streaming code into a utility, defined API response types (#175)
1 parent 711831b commit 1d2f1ab

12 files changed

Lines changed: 1079 additions & 775 deletions

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

Lines changed: 129 additions & 334 deletions
Large diffs are not rendered by default.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { useState, useCallback } from "react";
2+
import { Resource } from "@/types/resources";
3+
import {
4+
ActionPlan,
5+
PartialActionPlan,
6+
fetchActionPlanStreaming,
7+
} from "@/util/fetchActionPlan";
8+
9+
export interface UseActionPlanStreamingReturn {
10+
isGeneratingActionPlan: boolean;
11+
isStreamingActionPlan: boolean;
12+
streamingPlan: PartialActionPlan | null;
13+
actionPlan: ActionPlan | null;
14+
errorMessage: string | undefined;
15+
generateActionPlan: (
16+
selectedResources: Resource[],
17+
userEmail: string,
18+
clientDescription: string,
19+
) => Promise<{
20+
actionPlan: ActionPlan | null;
21+
resultId: string;
22+
errorMessage?: string;
23+
}>;
24+
setErrorMessage: (error: string | undefined) => void;
25+
clearActionPlan: () => void;
26+
}
27+
28+
/**
29+
* Custom hook to handle action plan streaming with SSE
30+
*/
31+
export function useActionPlanStreaming(): UseActionPlanStreamingReturn {
32+
const [isGeneratingActionPlan, setIsGeneratingActionPlan] = useState(false);
33+
const [isStreamingActionPlan, setIsStreamingActionPlan] = useState(false);
34+
const [streamingPlan, setStreamingPlan] = useState<PartialActionPlan | null>(
35+
null,
36+
);
37+
const [actionPlan, setActionPlan] = useState<ActionPlan | null>(null);
38+
const [errorMessage, setErrorMessage] = useState<string | undefined>(
39+
undefined,
40+
);
41+
42+
const generateActionPlan = useCallback(
43+
async (
44+
selectedResources: Resource[],
45+
userEmail: string,
46+
clientDescription: string,
47+
) => {
48+
if (selectedResources.length === 0) {
49+
return {
50+
actionPlan: null,
51+
resultId: "",
52+
errorMessage: "No resources selected",
53+
};
54+
}
55+
56+
setIsGeneratingActionPlan(true);
57+
setIsStreamingActionPlan(true);
58+
setActionPlan(null);
59+
setStreamingPlan(null);
60+
setErrorMessage(undefined);
61+
62+
try {
63+
const {
64+
actionPlan: finalPlan,
65+
resultId,
66+
errorMessage: planError,
67+
} = await fetchActionPlanStreaming(
68+
selectedResources,
69+
userEmail,
70+
clientDescription,
71+
// onChunk callback - receive structured partial plan
72+
(partialPlan: PartialActionPlan) => {
73+
setStreamingPlan(partialPlan);
74+
},
75+
// onComplete callback - stop streaming UI
76+
() => {
77+
setIsStreamingActionPlan(false);
78+
},
79+
// onError callback - clear content and show error
80+
(error: string) => {
81+
setIsStreamingActionPlan(false);
82+
setIsGeneratingActionPlan(false);
83+
setStreamingPlan(null);
84+
setActionPlan(null);
85+
setErrorMessage(error);
86+
},
87+
);
88+
89+
// Handle errors from the streaming response
90+
if (planError) {
91+
setErrorMessage(planError);
92+
setStreamingPlan(null);
93+
setActionPlan(null);
94+
return {
95+
actionPlan: null,
96+
resultId: "",
97+
errorMessage: planError,
98+
};
99+
}
100+
101+
// Set the final parsed action plan
102+
if (finalPlan) {
103+
setActionPlan(finalPlan);
104+
setStreamingPlan(null); // Clear streaming state
105+
return {
106+
actionPlan: finalPlan,
107+
resultId,
108+
};
109+
} else {
110+
const errorMsg =
111+
"There was an issue streaming the Action Plan. Please try again.";
112+
setErrorMessage(errorMsg);
113+
return {
114+
actionPlan: null,
115+
resultId: "",
116+
errorMessage: errorMsg,
117+
};
118+
}
119+
} catch (error) {
120+
console.error("Error generating action plan:", error);
121+
setIsStreamingActionPlan(false);
122+
setIsGeneratingActionPlan(false);
123+
setStreamingPlan(null);
124+
setActionPlan(null);
125+
const errorMsg =
126+
"There was an issue streaming the Action Plan. Please try again.";
127+
setErrorMessage(errorMsg);
128+
return {
129+
actionPlan: null,
130+
resultId: "",
131+
errorMessage: errorMsg,
132+
};
133+
} finally {
134+
setIsGeneratingActionPlan(false);
135+
}
136+
},
137+
[],
138+
);
139+
140+
const clearActionPlan = useCallback(() => {
141+
setActionPlan(null);
142+
setStreamingPlan(null);
143+
setIsStreamingActionPlan(false);
144+
setIsGeneratingActionPlan(false);
145+
setErrorMessage(undefined);
146+
}, []);
147+
148+
return {
149+
isGeneratingActionPlan,
150+
isStreamingActionPlan,
151+
streamingPlan,
152+
actionPlan,
153+
errorMessage,
154+
generateActionPlan,
155+
setErrorMessage,
156+
clearActionPlan,
157+
};
158+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useState, useCallback } from "react";
2+
import { Resource } from "@/types/resources";
3+
4+
export interface UseResourceRemovalReturn {
5+
recentlyRemoved: Resource | null;
6+
removedResourceIndex: number | null;
7+
handleRemoveResource: (
8+
resourceToRemove: Resource,
9+
currentResources?: Resource[],
10+
) => void;
11+
handleUndoRemove: (
12+
onRestore: (resource: Resource, index: number | null) => void,
13+
) => void;
14+
}
15+
16+
/**
17+
* Custom hook to handle resource removal and undo functionality
18+
*/
19+
export function useResourceRemoval(): UseResourceRemovalReturn {
20+
const [recentlyRemoved, setRecentlyRemoved] = useState<Resource | null>(null);
21+
const [removedResourceIndex, setRemovedResourceIndex] = useState<
22+
number | null
23+
>(null);
24+
25+
const handleRemoveResource = useCallback(
26+
(resourceToRemove: Resource, currentResources?: Resource[]) => {
27+
// Find and store the index before removing
28+
const index = currentResources?.findIndex(
29+
(r) => r.name === resourceToRemove.name,
30+
);
31+
if (index !== undefined && index !== -1) {
32+
setRemovedResourceIndex(index);
33+
}
34+
35+
setRecentlyRemoved(resourceToRemove);
36+
37+
// Auto-clear the undo notification after 7.5 seconds
38+
setTimeout(() => {
39+
setRecentlyRemoved((current) => {
40+
// If the resource is still marked as recently removed, clear it
41+
if (current === resourceToRemove) {
42+
return null;
43+
}
44+
return current;
45+
});
46+
setRemovedResourceIndex(null);
47+
}, 7500);
48+
},
49+
[],
50+
);
51+
52+
const handleUndoRemove = useCallback(
53+
(onRestore: (resource: Resource, index: number | null) => void) => {
54+
if (recentlyRemoved) {
55+
onRestore(recentlyRemoved, removedResourceIndex);
56+
setRecentlyRemoved(null);
57+
setRemovedResourceIndex(null);
58+
}
59+
},
60+
[recentlyRemoved, removedResourceIndex],
61+
);
62+
63+
return {
64+
recentlyRemoved,
65+
removedResourceIndex,
66+
handleRemoveResource,
67+
handleUndoRemove,
68+
};
69+
}

0 commit comments

Comments
 (0)