Skip to content

Commit 50ac0e4

Browse files
authored
Merge pull request #218 from codeforpdx/issue-215/update-letter-landing-page
[Enhancement] - Update Landing Page UI
2 parents 08a08be + 929abfc commit 50ac0e4

File tree

16 files changed

+984
-297
lines changed

16 files changed

+984
-297
lines changed

frontend/package-lock.json

Lines changed: 654 additions & 204 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
},
2323
"devDependencies": {
2424
"@eslint/js": "^9.22.0",
25+
"@testing-library/jest-dom": "^6.9.1",
2526
"@testing-library/react": "^16.3.0",
2627
"@types/react": "^19.0.10",
2728
"@types/react-dom": "^19.0.4",

frontend/src/Letter.tsx

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { useEffect, useRef, useState } from "react";
55
import { useParams } from "react-router-dom";
66
import { useLetterContent } from "./hooks/useLetterContent";
77
import { streamText } from "./pages/Chat/utils/streamHelper";
8-
import { buildLetterUserMessage } from "./pages/Chat/utils/letterHelper";
8+
import LetterGenerationDialog from "./pages/Letter/components/LetterGenerationDialog";
9+
import { buildLetterUserMessage } from "./pages/Letter/utils/letterHelper";
10+
import LetterDisclaimer from "./pages/Letter/components/LetterDisclaimer";
911

1012
export default function Letter() {
1113
const { addMessage, messages, setMessages } = useMessages();
@@ -15,6 +17,9 @@ export default function Letter() {
1517
const { org, loc } = useParams();
1618
const [startStreaming, setStartStreaming] = useState(false);
1719
const streamLocationRef = useRef<ILocation | null>(null);
20+
const [isLoading, setIsLoading] = useState(true);
21+
const dialogRef = useRef<HTMLDialogElement>(null);
22+
const LOADING_DISPLAY_DELAY_MS = 1000;
1823

1924
useEffect(() => {
2025
if (org === undefined) return;
@@ -49,55 +54,78 @@ export default function Letter() {
4954
runGenerateLetter();
5055
}, [messages, startStreaming, addMessage, setMessages]);
5156

57+
useEffect(() => {
58+
// Wait for the second message (index 1) which contains the initial AI response
59+
if (messages.length > 1 && messages[1]?.content !== "") {
60+
// Include 1s delay for smoother transition
61+
const timeoutId = setTimeout(
62+
() => setIsLoading(false),
63+
LOADING_DISPLAY_DELAY_MS,
64+
);
65+
return () => clearTimeout(timeoutId);
66+
}
67+
}, [messages]);
68+
69+
useEffect(() => {
70+
dialogRef.current?.showModal();
71+
}, []);
72+
5273
return (
53-
<div className="h-dvh pt-16 flex items-center">
54-
<div className="flex w-full items-center ">
55-
<div className="flex-1 transition-all duration-300">
56-
<div
57-
className={`container relative flex flex-col sm:flex-row gap-4 mx-auto p-6 bg-[#F4F4F2] rounded-lg shadow-[0_4px_6px_rgba(0,0,0,0.1)]
74+
<>
75+
<div className="h-dvh pt-16 flex items-center">
76+
<LetterGenerationDialog ref={dialogRef} />
77+
<div className="flex w-full items-center">
78+
<div className="flex-1 transition-all duration-300 relative">
79+
<div
80+
className={`container relative flex flex-col sm:flex-row gap-4 mx-auto p-6 bg-[#F4F4F2] rounded-lg shadow-[0_4px_6px_rgba(0,0,0,0.1)]
5881
${
5982
isOngoing
6083
? "justify-between h-[calc(100dvh-4rem-64px)] max-h-[calc(100dvh-4rem-64px)] sm:h-[calc(100dvh-10rem-64px)]"
6184
: "justify-center max-w-[600px]"
6285
}`}
63-
>
64-
{letterContent !== "" ? (
65-
<div className="flex flex-col gap-4 items-center flex-2/3 h-[40%] sm:h-full">
66-
<div className="overflow-y-scroll pr-4 w-full">
67-
<span
68-
className="whitespace-pre-wrap generated-letter"
69-
dangerouslySetInnerHTML={{
70-
__html: letterContent,
71-
}}
72-
/>
86+
>
87+
{letterContent !== "" ? (
88+
<div className="flex flex-col gap-4 items-center flex-2/3 h-[40%] sm:h-full">
89+
<div className="overflow-y-scroll pr-4 w-full">
90+
<span
91+
className="whitespace-pre-wrap generated-letter"
92+
dangerouslySetInnerHTML={{
93+
__html: letterContent,
94+
}}
95+
/>
96+
</div>
7397
</div>
98+
) : null}
99+
<div
100+
className={`flex flex-col ${letterContent === "" ? "flex-1" : "flex-1/3"} h-[60%] sm:h-full`}
101+
>
102+
{isLoading ? (
103+
<div className="flex flex-1 items-center justify-center animate-pulse text-lg">
104+
Generating letter...
105+
</div>
106+
) : (
107+
<MessageWindow
108+
messages={messages}
109+
addMessage={addMessage}
110+
location={location}
111+
setLocation={setLocation}
112+
setMessages={setMessages}
113+
isOngoing={isOngoing}
114+
/>
115+
)}
74116
</div>
75-
) : null}
117+
</div>
76118
<div
77-
className={`flex flex-col ${letterContent === "" ? "flex-1" : "flex-1/3"} h-[60%] sm:h-full`}
119+
className={`container mx-auto text-xs px-4 text-center ${isOngoing ? "max-w-auto my-2" : "max-w-[600px] my-4"}`}
78120
>
79-
<MessageWindow
80-
messages={messages}
81-
addMessage={addMessage}
82-
location={location}
83-
setLocation={setLocation}
84-
setMessages={setMessages}
85-
isOngoing={isOngoing}
86-
/>
121+
<p className={`${isOngoing ? "mb-0" : "mb-2"}`}>
122+
<LetterDisclaimer isOngoing={isOngoing} />
123+
</p>
124+
<p>For questions, contact michael@qiu-qiulaw.com</p>
87125
</div>
88126
</div>
89-
<div
90-
className={`container mx-auto text-xs px-4 text-center ${isOngoing ? "max-w-auto my-2" : "max-w-[600px] my-4"}`}
91-
>
92-
<p className={`${isOngoing ? "mb-0" : "mb-2"}`}>
93-
{isOngoing
94-
? "This chatbot offers general housing law info and is not legal advice. For help with your situation, contact a lawyer."
95-
: "The information provided by this chatbot is general information only and does not constitute legal advice. While Tenant First Aid strives to keep the content accurate and up to date, completeness and accuracy is not guaranteed. If you have a specific legal issue or question, consider contacting a qualified attorney or a local legal aid clinic for personalized assistance."}
96-
</p>
97-
<p>For questions, contact michael@qiu-qiulaw.com</p>
98-
</div>
99127
</div>
100128
</div>
101-
</div>
129+
</>
102130
);
103131
}

frontend/src/pages/Chat/components/MessageWindow.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import CitySelectField from "./CitySelectField";
77
import SuggestedPrompts from "./SuggestedPrompts";
88
import { ILocation } from "../../../hooks/useLocation";
99
import FeedbackModal from "./FeedbackModal";
10+
import { useLocation } from "react-router-dom";
1011

1112
interface Props {
1213
messages: IMessage[];
@@ -33,6 +34,13 @@ export default function MessageWindow({
3334
const [openFeedback, setOpenFeedback] = useState(false);
3435
const inputRef = useRef<HTMLTextAreaElement | null>(null);
3536
const messagesRef = useRef<HTMLDivElement | null>(null);
37+
const loc = useLocation();
38+
39+
// To hide initial prompt and response for letter generation
40+
const LETTER_PAGE_HIDDEN_MESSAGES = 2;
41+
const displayedMessages = loc.pathname.startsWith("/letter")
42+
? messages.slice(LETTER_PAGE_HIDDEN_MESSAGES)
43+
: messages;
3644

3745
const handleClearSession = () => {
3846
window.location.reload();
@@ -69,25 +77,27 @@ export default function MessageWindow({
6977
>
7078
<div className="max-h-[calc(100dvh-240px)] sm:max-h-[calc(100dvh-20rem)] mx-auto max-w-[700px]">
7179
{isOngoing ? (
72-
<div className="flex flex-col gap-4">
73-
{messages.map((message) => (
74-
<div
75-
className={`flex w-full ${
76-
message.role === "model" ? "justify-start" : "justify-end"
77-
}`}
78-
key={message.messageId}
79-
>
80+
<div className="flex flex-col gap-4 relative">
81+
{displayedMessages.map((message) => {
82+
return (
8083
<div
81-
className={`message-bubble p-3 rounded-2xl max-w-[95%] ${
82-
message.role === "model"
83-
? "bg-slate-200 rounded-tl-sm"
84-
: "bg-[#1F584F] text-white rounded-tr-sm"
84+
className={`flex w-full ${
85+
message.role === "model" ? "justify-start" : "justify-end"
8586
}`}
87+
key={message.messageId}
8688
>
87-
<MessageContent message={message} />
89+
<div
90+
className={`message-bubble p-3 rounded-2xl max-w-[95%] ${
91+
message.role === "model"
92+
? "bg-slate-200 rounded-tl-sm"
93+
: "bg-[#1F584F] text-white rounded-tr-sm"
94+
}`}
95+
>
96+
<MessageContent message={message} />
97+
</div>
8898
</div>
89-
</div>
90-
))}
99+
);
100+
})}
91101
</div>
92102
) : null}
93103
</div>

frontend/src/pages/Chat/utils/letterHelper.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Link } from "react-router-dom";
2+
3+
interface Props {
4+
isOngoing: boolean;
5+
}
6+
7+
export default function LetterDisclaimer({ isOngoing }: Props) {
8+
return isOngoing ? (
9+
<span>
10+
<strong>Disclaimer</strong>: This tool provides general information and
11+
drafts letters based solely on what you enter. It is not legal advice and
12+
does not create an attorney–client relationship. As explained further in
13+
the{" "}
14+
<Link to="/privacy-policy" target="_blank" className="underline">
15+
Privacy Policy
16+
</Link>
17+
, we do not save any data from these conversations, but you can enter your
18+
personal information into the chatbox and it will appear in the
19+
corresponding brackets of the letter.
20+
</span>
21+
) : (
22+
<span>
23+
The information provided by this chatbot is general information only and
24+
does not constitute legal advice. While Tenant First Aid strives to keep
25+
the content accurate and up to date, completeness and accuracy is not
26+
guaranteed. If you have a specific legal issue or question, consider
27+
contacting a qualified attorney or a local legal aid clinic for
28+
personalized assistance.
29+
</span>
30+
);
31+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
interface Props {
2+
ref: React.RefObject<HTMLDialogElement | null>;
3+
}
4+
5+
export default function LetterGenerationDialog({ ref }: Props) {
6+
return (
7+
<dialog
8+
ref={ref}
9+
aria-label="letter-dialog-modal"
10+
aria-labelledby="letter-dialog-title"
11+
aria-describedby="letter-dialog-description"
12+
className="rounded-lg p-6 min-w-[300px] max-w-[600px] fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
13+
>
14+
<div className="flex flex-col items-end">
15+
<h2 id="letter-dialog-title" className="sr-only">
16+
Letter Generation Notice
17+
</h2>
18+
<p id="letter-dialog-description">
19+
You've been redirected here so we can help you create a letter to your
20+
landlord. It'll take a few seconds to complete your initial letter.
21+
You could instruct the tool to update the letter to your liking after
22+
it's generated. Once your letter is complete, you should go back to
23+
your previous page and follow the remaining steps.
24+
</p>
25+
<button
26+
onClick={() => ref.current?.close()}
27+
className="cursor-pointer underline text-blue-600 hover:text-blue-500 text-sm"
28+
>
29+
close
30+
</button>
31+
</div>
32+
</dialog>
33+
);
34+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
CitySelectOptions,
3+
type CitySelectOptionType,
4+
} from "../../Chat/components/CitySelectField";
5+
6+
interface IBuildLetterReturnType {
7+
userMessage: string;
8+
selectedLocation: CitySelectOptionType;
9+
}
10+
11+
function buildLetterUserMessage(
12+
org: string,
13+
loc: string | undefined,
14+
): IBuildLetterReturnType | null {
15+
const selectedLocation = CitySelectOptions[loc || "oregon"];
16+
if (selectedLocation === undefined) return null;
17+
const locationString =
18+
selectedLocation.city && selectedLocation.state
19+
? `${selectedLocation.city}, ${selectedLocation.state}`
20+
: selectedLocation.city || selectedLocation.state?.toUpperCase() || "";
21+
const sanitizedOrg = org
22+
.replace(/[<>'"{}[\]]/g, "")
23+
.trim()
24+
.slice(0, 100);
25+
const promptParts = [
26+
`Hello, I've been redirected from ${sanitizedOrg}.`,
27+
`Draft a letter related to housing issues for my area${locationString ? ` (${locationString})` : ""} to my landlord.`,
28+
`Use the information in this prompt to generate a letter to my landlord.`,
29+
`The issue could be maintenance issues, unsafe conditions, or anything else affecting my home, use a broken faucet as an example.`,
30+
`Update the letter as we discuss.`,
31+
`Update all placeholders for city and state in the letter with${locationString ? ` (${locationString})` : ""}`,
32+
`When all but the signature placeholder have been replaced, please confirm that I have proof-read the letter for accuracy in content and tone,`,
33+
`provide instructions for how to copy and paste(formatted) the letter from the browser into a application of my choice,`,
34+
`the necessary and optional notification / deliveries to the recipient(s), and retention / receipt best practices.`,
35+
`Have the user follow the steps mention from ${sanitizedOrg} first after letter completion, if there were any.`,
36+
];
37+
38+
return {
39+
userMessage: promptParts.join(" "),
40+
selectedLocation,
41+
};
42+
}
43+
44+
export { buildLetterUserMessage };

0 commit comments

Comments
 (0)