Skip to content

Commit 9a297e2

Browse files
authored
Merge pull request #242 from codeforpdx/issue-228/update-initial-form
[Enhancement] - Update Initial Form
2 parents e183bdc + 8f23c8c commit 9a297e2

28 files changed

+881
-243
lines changed

Architecture.md

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -437,40 +437,59 @@ frontend/
437437
│ ├── PrivacyPolicy.tsx # Privacy policy
438438
│ ├── main.tsx # Application entry point
439439
│ ├── style.css # Global styles
440+
│ ├── contexts/ # React Contexts
441+
│ │ └── HousingContext.tsx # Housing context for chat/letter generation
440442
│ ├── hooks/ # Custom React hooks
441443
│ │ ├── useMessages.tsx # Message handling logic
442-
│ │ ├── useSession.tsx # Session management
444+
│ │ ├── useHousingContext.tsx # Custom hook for housing context
443445
│ │ └── useLetterContent.tsx # State management for letter generation
444446
│ ├── pages/Chat/ # Chat page components
445447
│ │ ├── components/
446-
│ │ │ ├── CitySelectField.tsx # Location selection
448+
│ │ │ ├── ChatDisclaimer.tsx # Disclaimer for Chat page
449+
│ │ │ ├── InitializationForm.tsx # Context information from user
447450
│ │ │ ├── ExportMessagesButton.tsx # Chat export
448451
│ │ │ ├── InputField.tsx # Message input
449452
│ │ │ ├── FeedbackModal.tsx # Feedback modal
450453
│ │ │ ├── MessageContent.tsx # Message display
451454
│ │ │ ├── MessageWindow.tsx # Chat window
455+
│ │ │ ├── SelectField.tsx # Initialization form select field
452456
│ │ │ └── SuggestedPrompts.tsx # Prompt suggestions
453457
│ │ └── utils/
454458
│ │ ├── exportHelper.ts # Export functionality
455459
│ │ ├── feedbackHelper.tsx # Feedback functionality
460+
│ │ ├── formHelper.tsx # Housing context functionality
456461
│ │ └── streamHelper.tsx # Stream functionality
462+
│ ├── pages/Letter/ # Letter page components
463+
│ │ ├── components/
464+
│ │ │ ├── LetterDisclaimer.tsx # Disclaimer for Letter page
465+
│ │ │ └── LetterGenerationDialog.tsx # Letter page dialog
466+
│ │ └── utils/
467+
│ │ └── letterHelper.tsx # Letter generation functionality
457468
│ └── shared/ # Shared components and utils
458469
│ │ ├── components/
459470
│ │ │ ├── BackLink.tsx # Navigation component
460471
│ │ │ ├── BeaverIcon.tsx # Oregon-themed icon
461472
│ │ │ ├── DisclaimerLayout.tsx # Layout for disclaimer components
462473
│ │ │ ├── MessageContainer.tsx # Layout for main UI component
463474
│ │ │ ├── Navbar.tsx # Navigation
464-
│ │ │ └── TenatFirstAidLogo.tsx # Application logo
475+
│ │ │ └── TenantFirstAidLogo.tsx # Application logo
465476
│ │ ├── constants/
466477
│ │ │ └── constants.ts # File of constants
467478
│ │ └── utils/
468479
│ │ └── dompurify.ts # Helper function for sanitizing text
469-
│ └── tests/
470-
│ │ ├── components/
471-
│ │ │ └── Chat.test.tsx # Chat component testing
472-
│ │ └── utils/
480+
│ └── tests/ # Testing suite
481+
│ │ ├── components/ # Component testing
482+
│ │ │ ├── About.test.ts # About component testing
483+
│ │ │ ├── Chat.test.tsx # Chat component testing
484+
│ │ │ ├── ChatDisclaimer.test.ts # ChatDisclaimer component testing
485+
│ │ │ ├── HousingContext.test.ts # HousingContext component testing
486+
│ │ │ ├── InitializationForm.test.ts # InitializationForm component testing
487+
│ │ │ ├── Letter.test.ts # Letter component testing
488+
│ │ │ ├── LetterDisclaimer.test.ts # LetterDisclaimer component testing
489+
│ │ │ ├── MessageWindow.test.ts # MessageWindow component testing
490+
│ │ └── utils/ # Utility function testing
473491
│ │ ├── letterHelper.test.ts # letterHelper testing
492+
│ │ ├── formHelper.test.ts # formHelper testing
474493
│ │ └── dompurify.test.ts # dompurify testing
475494
├── public/
476495
│ └── favicon.svg # Site favicon

backend/tenantfirstaid/chat.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,47 @@
2626
https://oregon.public.law/statutes
2727
https://www.portland.gov/code/30/01
2828
https://eugene.municipal.codes/EC/8.425
29-
Include the links inline in your answer, with the attribute target="_blank" so that they open in a new tab, likethis:
29+
Include the links inline in your answer, with the attribute target="_blank" so that they open in a new tab, like this:
3030
<a href="https://oregon.public.law/statutes/ORS_90.427" target="_blank">ORS 90.427</a>.
3131
3232
If the user asks questions about Section 8 or the HomeForward program, search the web for the correct answer and provide a link to the page you used, using the same format as above.
3333
34-
If the user asks to make/generate/create/draft a letter, you should return a formatted letter after your conversational response. Add a delimiter -----generate letter----- to separate the two content. You can include <a>, <em>, and <strong> tags for additional formatting.
34+
If the user asks to make/generate/create/draft a letter, you should return a formatted letter after your conversational response. Add a delimiter -----generate letter----- to separate the two content. Place this formatted letter at the end of the response. You can include <a>, <em>, and <strong> tags for additional formatting. Proof-read the letter for accuracy in content and tone.
35+
36+
You can use the following as the initial letter template:
37+
38+
[Your Name]
39+
[Your Street Address]
40+
[Your City, State, Zip Code]
41+
[Date]
42+
43+
<strong>Via First-Class Mail and/or Email</strong>
44+
45+
[Landlord's Name or Property Management Company]
46+
[Landlord's or Property Manager's Street Address]
47+
[Landlord's or Property Manager's City, State, Zip Code]
48+
49+
<strong>Re: Request for Repairs at [Your Street Address]</strong>
50+
51+
Dear [Landlord's Name], I am writing to request immediate repairs for the property I rent at [Your Street Address]. I am making this request pursuant to my rights under the Oregon Residential Landlord and Tenant Act.
52+
53+
As of [Date you first noticed the problem], I have observed the following issues that require your attention:
54+
55+
• [Clearly describe the problem. For example: "The faucet in the kitchen sink constantly drips and will not turn off completely."]
56+
• [Continue to list problems, if any]
57+
58+
These conditions are in violation of your duty to maintain the premises in a habitable condition as required by Oregon law, specifically ORS 90.320.
59+
60+
I request that you begin making repairs to address these issues within [number of days] days. Please contact me at [Your Phone Number] or [Your Email Address] to schedule a time for the repairs to be made.
61+
62+
I look forward to your prompt attention to this matter.
63+
64+
Sincerely,
65+
66+
[Your Name]
67+
68+
69+
If they provide details replace the issue in the template.
3570
"""
3671

3772

@@ -40,11 +75,21 @@ def __init__(self):
4075
self.client = genai.Client(vertexai=True)
4176

4277
def prepare_developer_instructions(self, city: str, state: str) -> str:
78+
VALID_CITIES = {"Portland", "Eugene", "null", None}
79+
VALID_STATES = {"OR"}
80+
81+
# Validate and sanitize inputs
82+
city_clean = city.title() if city else "null"
83+
state_upper = state.upper() if state else "OR"
84+
85+
if city_clean not in VALID_CITIES:
86+
city_clean = "null"
87+
if state_upper not in VALID_STATES:
88+
raise ValueError(f"Invalid state: {state}")
89+
4390
# Add city and state filters if they are set
4491
instructions = DEFAULT_INSTRUCTIONS
45-
instructions += (
46-
f"\nThe user is in {city if city != 'null' else ''} {state.upper()}.\n"
47-
)
92+
instructions += f"\nThe user is in {city_clean if city_clean != 'null' else ''} {state_upper}.\n"
4893
return instructions
4994

5095
def generate_gemini_chat_response(

backend/tests/test_chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def chat_manager(mocker):
2727

2828
def test_prepare_developer_instructions_includes_city_state(chat_manager):
2929
city = "Portland"
30-
state = "or"
30+
state = "OR"
3131
instructions = chat_manager.prepare_developer_instructions(city, state)
3232
assert f"The user is in {city} {state.upper()}." in instructions
3333

frontend/src/Chat.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
import MessageWindow from "./pages/Chat/components/MessageWindow";
22
import useMessages from "./hooks/useMessages";
3-
import useLocation from "./hooks/useLocation";
43
import { useLetterContent } from "./hooks/useLetterContent";
54
import ChatDisclaimer from "./pages/Chat/components/ChatDisclaimer";
65
import MessageContainer from "./shared/components/MessageContainer";
76

87
export default function Chat() {
98
const { addMessage, messages, setMessages } = useMessages();
10-
const { location, setLocation } = useLocation();
119
const isOngoing = messages.length > 0;
1210
const { letterContent } = useLetterContent(messages);
1311

1412
return (
15-
// Chat layout is adjusted before and after session starts to account for CitySelectField
16-
<div
17-
className={`absolute ${isOngoing ? "top-16 md:top-32" : "top-1/2 -translate-y-1/2"} w-full flex items-center`}
18-
>
13+
<div className="flex pt-16 h-screen items-center justify-center">
1914
<div className="flex-1 h-full sm:h-auto items-center transition-all duration-300">
2015
<MessageContainer isOngoing={isOngoing} letterContent={letterContent}>
2116
<div
@@ -24,8 +19,6 @@ export default function Chat() {
2419
<MessageWindow
2520
messages={messages}
2621
addMessage={addMessage}
27-
location={location}
28-
setLocation={setLocation}
2922
setMessages={setMessages}
3023
isOngoing={isOngoing}
3124
/>

frontend/src/Letter.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import MessageWindow from "./pages/Chat/components/MessageWindow";
22
import useMessages from "./hooks/useMessages";
3-
import useLocation, { ILocation } from "./hooks/useLocation";
43
import { useEffect, useRef, useState } from "react";
54
import { useParams } from "react-router-dom";
65
import { useLetterContent } from "./hooks/useLetterContent";
@@ -9,10 +8,12 @@ import LetterGenerationDialog from "./pages/Letter/components/LetterGenerationDi
98
import { buildLetterUserMessage } from "./pages/Letter/utils/letterHelper";
109
import LetterDisclaimer from "./pages/Letter/components/LetterDisclaimer";
1110
import MessageContainer from "./shared/components/MessageContainer";
11+
import useHousingContext from "./hooks/useHousingContext";
12+
import { buildChatUserMessage } from "./pages/Chat/utils/formHelper";
13+
import { ILocation } from "./contexts/HousingContext";
1214

1315
export default function Letter() {
1416
const { addMessage, messages, setMessages } = useMessages();
15-
const { location, setLocation } = useLocation();
1617
const isOngoing = messages.length > 0;
1718
const { letterContent } = useLetterContent(messages);
1819
const { org, loc } = useParams();
@@ -21,20 +22,35 @@ export default function Letter() {
2122
const [isLoading, setIsLoading] = useState(true);
2223
const dialogRef = useRef<HTMLDialogElement>(null);
2324
const LOADING_DISPLAY_DELAY_MS = 1000;
25+
const { housingLocation, housingType, tenantTopic, issueDescription } =
26+
useHousingContext();
27+
const { userMessage: initialUserMessage } = buildChatUserMessage(
28+
housingLocation,
29+
housingType,
30+
tenantTopic,
31+
issueDescription,
32+
);
2433

2534
useEffect(() => {
2635
const output = buildLetterUserMessage(org, loc);
2736
if (output === null) return;
37+
const hasIssueContext = issueDescription !== "";
2838

2939
const userMessageId = Date.now().toString();
3040
// Add user message
3141
setMessages((prev) => [
3242
...prev,
33-
{ role: "user", content: output.userMessage, messageId: userMessageId },
43+
{
44+
role: "user",
45+
content: [hasIssueContext ? initialUserMessage : "", output.userMessage]
46+
.join(" ")
47+
.trim(),
48+
messageId: userMessageId,
49+
},
3450
]);
3551
streamLocationRef.current = output.selectedLocation;
3652
setStartStreaming(true);
37-
}, [loc, org, setMessages]);
53+
}, [loc, org, setMessages, issueDescription, initialUserMessage]);
3854

3955
useEffect(() => {
4056
if (startStreaming === false || streamLocationRef.current === null) return;
@@ -46,7 +62,7 @@ export default function Letter() {
4662
const streamDone = await streamText({
4763
addMessage,
4864
setMessages,
49-
location: streamLocationRef.current,
65+
housingLocation: streamLocationRef.current,
5066
});
5167
const INITIAL_INSTRUCTION =
5268
"What was generated is just an initial template. Please include details of your specific housing situation to update the letter.";
@@ -96,7 +112,7 @@ export default function Letter() {
96112

97113
return (
98114
<>
99-
<div className="absolute top-16 md:top-32 w-full flex items-center">
115+
<div className="flex pt-16 h-screen items-center justify-center">
100116
<LetterGenerationDialog ref={dialogRef} />
101117
<div className="flex-1 h-full sm:h-auto items-center transition-all duration-300">
102118
<MessageContainer isOngoing={isOngoing} letterContent={letterContent}>
@@ -111,8 +127,6 @@ export default function Letter() {
111127
<MessageWindow
112128
messages={messages}
113129
addMessage={addMessage}
114-
location={location}
115-
setLocation={setLocation}
116130
setMessages={setMessages}
117131
isOngoing={isOngoing}
118132
/>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createContext, useCallback, useMemo, useState } from "react";
2+
3+
export interface ILocation {
4+
city: string | null;
5+
state: string | null;
6+
}
7+
8+
export interface IHousingContextType {
9+
housingLocation: ILocation;
10+
city: string | null;
11+
housingType: string | null;
12+
tenantTopic: string | null;
13+
issueDescription: string;
14+
handleHousingLocation: ({ city, state }: ILocation) => void;
15+
handleCityChange: (option: string | null) => void;
16+
handleHousingChange: (option: string | null) => void;
17+
handleTenantTopic: (option: string | null) => void;
18+
handleIssueDescription: (
19+
event: React.ChangeEvent<HTMLTextAreaElement>,
20+
) => void;
21+
}
22+
23+
const HousingContext = createContext<IHousingContextType | null>(null);
24+
25+
interface Props {
26+
children: React.ReactNode;
27+
}
28+
29+
export default function HousingContextProvider({ children }: Props) {
30+
const [city, setCity] = useState<string | null>(null);
31+
const [housingLocation, setHousingLocation] = useState<ILocation>({
32+
city: null,
33+
state: null,
34+
});
35+
const [housingType, setHousingType] = useState<string | null>(null);
36+
const [tenantTopic, setTenantTopic] = useState<string | null>(null);
37+
const [issueDescription, setIssueDescription] = useState("");
38+
39+
const handleHousingLocation = useCallback(({ city, state }: ILocation) => {
40+
setHousingLocation({ city, state });
41+
}, []);
42+
43+
const handleCityChange = useCallback((option: string | null) => {
44+
setCity(option);
45+
}, []);
46+
47+
const handleHousingChange = useCallback((option: string | null) => {
48+
setHousingType(option);
49+
}, []);
50+
51+
const handleTenantTopic = useCallback((option: string | null) => {
52+
setTenantTopic(option);
53+
}, []);
54+
55+
const handleIssueDescription = useCallback(
56+
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
57+
setIssueDescription(event.target.value);
58+
},
59+
[],
60+
);
61+
62+
const housingContextObject = useMemo(
63+
() => ({
64+
housingLocation,
65+
city,
66+
housingType,
67+
tenantTopic,
68+
issueDescription,
69+
handleHousingLocation,
70+
handleCityChange,
71+
handleHousingChange,
72+
handleTenantTopic,
73+
handleIssueDescription,
74+
}),
75+
[
76+
housingLocation,
77+
city,
78+
housingType,
79+
tenantTopic,
80+
issueDescription,
81+
handleHousingLocation,
82+
handleCityChange,
83+
handleHousingChange,
84+
handleTenantTopic,
85+
handleIssueDescription,
86+
],
87+
);
88+
89+
return (
90+
<HousingContext.Provider value={housingContextObject}>
91+
{children}
92+
</HousingContext.Provider>
93+
);
94+
}
95+
96+
export { HousingContext };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useContext } from "react";
2+
import { HousingContext } from "../contexts/HousingContext";
3+
4+
export default function useHousingContext() {
5+
const context = useContext(HousingContext);
6+
if (!context) {
7+
throw new Error(
8+
"useHousing can only be used within HousingContextProvider",
9+
);
10+
}
11+
return context;
12+
}

0 commit comments

Comments
 (0)