musings about life, tech, & more
- kevin le
- Auto Scrolling To The Bottom of A ChatboxKevin Le
In my attempt to implement a chatgpt-like bot that streams responses, I recently had to make the container that I had placed my OpenAI response in have a static height & make it scrollable when overfilled on the y-axis. For better user experience, I also had to have the container auto scroll to the bottom as the result is streaming.
Here's how I did it.
'use client'; import { useState, useRef } from 'react'; export default function Chat({ simplifiedTracks }: Props) { const [isGeneratingResponse, setIsGeneratingResponse] = useState(false); const [results, setResults] = useState(''); const chatBoxRef = useRef<HTMLDivElement>(); async function handleSubmit( event: React.SyntheticEvent, simplifiedTracks: SimplifiedTrack[] ) { // clear results on subsquent clicks setResults(''); event.preventDefault(); // disable the button to prevent users from sending a request while one is currently active setIsGeneratingResponse(true); const res = await streamResponse(simplifiedTracks); // double click does nothing because of our rate limited backend if (res.status === 429) { setResults(''); return; } const reader = res.body?.pipeThrough(new TextDecoderStream()).getReader(); let done = false; while (!done) { const { value, done: doneReading } = await reader?.read(); done = doneReading; if (value != undefined) { setResults((prev) => prev + value); // scroll to the bottom when new content is added chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight; } } // allow user to click the button again setIsGeneratingResponse(false); } return ( <section className="w-4/5 md:w-3/4 flex flex-col items-center"> <div className="max-h-24 whitespace-break-spaces w-full overflow-y-scroll" ref={chatBoxRef} > <p> {results} {/* <div>scrolling to this</div> */} </p> </div> <form onSubmit={(e) => handleSubmit(e, simplifiedTracks)}> <div className="w-24"></div> <button type="submit" className="justify-center items-center hover:underline hover mt-4 p-4 border-2 rounded-lg w-full bg-teal-400" disabled={isGeneratingResponse} > {isGeneratingResponse ? ( <LinearProgress sx={{ width: '100%', }} /> ) : ( 'Judge your music taste' )} </button> </form> </section> }
Let's break down the code that makes this work, specifically the auto scrolling box.
Prerequisites:
Our component needs to be a client component, hence
'use client';
Why? In the Next.js 13 App Router, all components are by default Server Components. In server components, we don't have access to the hooks that we're accustomed to using, like useState, useEffect, and the hook that we're about to use to implement this auto-scrolling feature, useRef. We can bypass this by marking the component with the 'use client' directive in line 1.
const chatBoxRef = useRef<HTMLDivElement>(); <div className="max-h-24 whitespace-break-spaces w-full overflow-y-scroll" ref={chatBoxRef} > // we create our ref, which we use to reference a specific dom element, and attach the ref to our chatbox. we choose this specific div because this is the container that is overflowing.
To establish a static height for this container, I added a max height via max-h-24, and to make it scrollable, I used tailwind's overflow-y-scroll utility class.
I originally tried to assign the container a height of h-1/2, or height:50% but that did not work because its parent container is a flex container with no specified height. I needed to set a static height as opposed to using a relative height, hence max-h-24.
// scroll to the bottom when new content is added chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight; // scrollTop signifies the current scroll position of the box. // scrollHeight represents the full height of the box. // when we set the current scroll position to the height of the box as the height of the box is increasing, we emulate the auto-scroll feature that is so prevalent in chat apps.
I hope this helped!
Tags: - initial setup for my newest project using ChatGPT API & SpotifyKevin Le
Designing an app from the ground up is very time consuming.
A part of me just wants to hop straight into the coding. I want to just make my components and iterate from there, but I've seen where that takes me, and it's usually not where I want to go.
I've learned that in order to stick to a project over a longer term and not just give up on it soon-after I start, I have to spend time in the planning process, and take that part seriously. Otherwise, I just hop straight into the code and run out of steam when I lose direction.
When dealing with full-stack applications, there's a ton of moving parts and small little connections that need to be made. It's too easy to get overwhelmed by all there is to do, but I find it best to use Trello break these tasks into bite sized chunks that don't give me anxiety when I try to solve them.
I spent most of my time today establishing a direction & brainstorming a more concrete idea of what I want to be making.
This is me brainstorming and breaking down my app into an MVP-sized chunks.
The process is very algorithmic. Just like how I would solve a leetcode problem. Do not just jump right into the code. Plan, and then execute.
Tags:Tech - I Just Wrote Possibly My Best Component Yet.Kevin Le
'use client' import Link from 'next/link' import { Tooltip } from '@chakra-ui/react' interface Props { links: { [key: string]: { link: string label: string icon?: JSX.Element } } } export default function Links({ links }: Props) { return ( <div className='mx-20 flex w-full gap-2'> {Object.entries(links).map(([key, { link, label, icon }]) => ( <Tooltip key={key} label={label} fontSize='lg' placement='bottom'> <Link key={key} href={link} className='font-extralight text-white'> {icon} </Link> </Tooltip> ))} </div> ) }
I may have written my best React component yet.
When I first started writing React, I had very little knowledge of JavaScript and HTML. I had basically went into it thinking that React was just a easy way to render markdown on a page, and generally was just used because it provided such quality-of-life over more primitive tools like JQuery.
There are rules to designing good components. I was originally taught that React components could be thought of as Lego blocks. We stack up these blocks to build a website. A <Navbar/> here, some <Link/>'s there, some <Content/> here, a <Footer /> to top it off and bam, you have a website.
Little did I know that as with anything, there are design principles that make up "good" code or "good" components.
(fill in the rest here) (talk about modularity, single responsibility, MVC, pure functions, input/output etc)
Initial Navbar Component:
Updated Navbar Component:
10:31PM - Definitely an improvement over the first version. We can use component composition via the children prop to avoid prop drilling of the blogLinks object. Also better practice to extract lists into their own components.
The benefits: Navbar is basically now a container for whatever we want to be in the navbar. We could add another set of links, or we could add a title or a logo and we could just slot that right into the children prop.
Tags:Tech - Web Analytics, and YouKevin Le
I've always had a slight interest in SEO, but never really explored it until today. It's a lot easier to be concerned with metrics when you have a deployed site that you built, and for some reason I never thought to track how much traffic my site was getting since I built it.
Luckily, Vercel makes it extremely easy to get started with data collection for your website.
All I needed to do was install a package with NPM.
npm i @vercel/analytics
Then add the Analytics component that comes along with it.
// src/app/layout.tsx import { Analytics } from '@vercel/analytics/react' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang='en' className='dark'> <body className='flex min-h-screen min-w-[320px] flex-col bg-lightPrimary dark:bg-darkPrimary dark:text-lightPrimary'> <Providers> {children} <Analytics /> </Providers> </body> </html> ) }
After adding this component to my app, now whenever someone visits my website, a unique hash is generated and a POST request is sent to Vercel's vitals endpoint
Normally, users are tracked via cookies. You know how every website has that popup asking for your permission to Accept Cookies?
Yeah, I get that a bit more now. Businesses want information on your clicking habits on their page to influence their decision-making!
Reminds me of a time I implemented a click tracker with React and UseContext, but I digress.
We need data to see what works. Metrics power everything, whether we know it or not, and maintaining actual data like web traffic is vital to success in the tech industry.
Maybe after I post this on Linkedin my web traffic will go up... or not.
Tags:Tech - i should be able to see this from my prod blogKevin Le
i should be able to see this from kevinle.xyz/blog
this isn't good... for some reason my production blog can't see my most recent post, even though my localhost can.
let's get to the bottom of this...
UPDATE:
i think i figured it out. has something to do with how my data/posts are getting cached. in order to conserve API calls, NextJS aggressively caches data. The problem was that my data was never getting revalidated, so the cache was stale.
I believe adding a simple option to the Next.js fetch call will fix my problems.
const allPosts = await clientFetch( postsQuery, {}, { next: { tags: ['blog'], revalidate: 30 } } )
I believe now after 30 seconds, the cache will revalidate itself so it will catch new posts without me having to rebuild the app.
I have a lot to learn about NextJS caching.
This Next.js documentation on Caching is a hefty read and I have yet to fully grasp everything Next is doing for us out of the box.
What I'm doing above is called Time-based Revalidation, which technically works for now, but I think I may have to put in some more effort to switch over to an On-demand Revalidation of my data, seeing as I want my latest articles to be shown ASAP.
I would consider this a rabbit hole, but hey, I love learning about backend. Would much rather do this than center a div for sure.
Tags:Tech - improving the blog by adding code blocksKevin Le
This will be a very meta blog post, as I'm making a blog post about how I'm improving the blog through the addition of me being able to add code blocks to my Sanity schemas.
Natural of any tech blog, I'd like to be able to share code snippets that help document my learning as well as help others should they run into the same issues as I did.
Turns out developers like myself had already thought of a solution and it was just up to me to apply it to my situation.
As I've commonly found in my software engineering endeavors, the answer to my problem could be found in a simple google search.
This npm package provides me an input type of "code" that
I placed inside of my sanity studio schema.
defineArrayMember({ type: 'code', }),
After I placed a test code block into one of my blog posts, it wouldn't render onto the page, because my portableText wasn't able to detect & serialize (turn into rich HTML) the new content of type "code".
Basically what we need in order to transform the content block of type "code" into rich text, is to take it and render a component whenever we run into that block, so I made a custom <Code> component that would take one of the objects that Sanity spits out and spits out the markdown.
const myPortableTextComponents: Partial<PortableTextReactComponents> = { types: { image: ({ value }) => { return <SanityImage {...value} /> }, code: ({ value }) => { return <Code node={...value} /> }, }, marks: { link: ({ value, children }) => { return ( <Link href={value?.href} target={'_blank'}> {children} </Link> ) }, }, }
I soon after realized that I needed a syntax highlighter, otherwise my code blocks wouldn't be easy to read. I found many options for such a tool, but I ultimately decided on React-Syntax-Highlighter.
'use client' import SyntaxHighlighter from 'react-syntax-highlighter' import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs' export default function Code({ node }) { console.log(node, 'node') if (!node || !node.code) { return null } const { language, code } = node return ( <SyntaxHighlighter language={language} style={atomOneDark}> {code} </SyntaxHighlighter> ) }
and voila, the beautiful code snippets that you see on this post weren't possible before today.
Hope you learned a thing or two, and catch you on the flip.
Tags:Tech - Blog MotivationsKevin Le
I guess this is my first post. Let me talk about my intentions and motivations behind making this content and why I even bothered to spend time setting up this blog from scratch instead of making my content on say, Medium or whatever.
To be honest, I thought making this blog was going to be real easy. It's just a basic crud app, right? Some article component, render text onto a page, whatever. As I was sketching things and really fleshing out the plan for the blog, I quickly realized that this was going to be something that would require a bit more attention than I had thought.
For example, if my content was going to be hosted on my page at kevinle.xyz/blog, obviously the articles I would write would be dynamically pulled from some hosted DB option (like Supabase), and I would have to code up the frontend forms to take my content input.
This already sounded like overkill. I think the thing that really pushed it over the top was when I realized that I would need to implement some login feature for my own portfolio site, to establish myself as an admin, just to dynamically display some content on my page. It's not like I couldn't do it, but something felt wrong with the idea of implementing this all from scratch, like I was trying too hard to reinvent the wheel.
After a quick google search, I discovered that what I had been trying to implement myself was basically a CMS. One source of content displayed on a page for everyone to see. Sanity.io had the perfect solution for me. They provided me with a fully customizable (using React) content management page that they call the "Sanity Studio", that I could use to edit my blog posts. Images are hosted on their CDN so I don't have to worry about hosting the images/data myself. The added bonus being that the auth is built in, too, so only I have access to my studio.
Blog posts (like the one you're reading now) are pulled from the Sanity Content Lake, using their built in query language, GROQ.
GROQ took a little time to understand, because I had never really interacted with a type of querying that was so similar to GraphQL's, but after a few days and some toying around, I'm able to query, filter, and project my JSON payloads to be exactly how I want them to be.
The only downside of GROQ, in my opinion, is that the queries have no TypeScript autocomplete, which makes runtime errors a lot more common than I would like.
Overall, I'm very happy with how this turned out. I learned so much about Content Management Systems. The possibilities are endless, because content is everywhere. All this I never would've learned had I just stuck to making content on Medium.
Thanks for reading.
Tags:Tech - Thoughts on BalanceKevin Le
These days I've been thinking a lot about balance.
Often times my mind bounces between these two conflicting worldviews.
One of these worldviews being that life is meant to be enjoyed, that it's okay to relax and take your foot off the gas. I like to believe in the idea of slow growth, and that progress need not be rushed, and that great things take time, and I could really stand to be more patient with myself.
On the other end of the extreme is the falling into "hustle culture". Some of the many possible tenets of this mentality which I'll list out right now: "grind till you can't anymore", "sacrifice is a necessity to get what you want", "you must suffer to succeed".
The conclusion that I've arrived at, like so many others before me, is that either of these approaches is very hyperbolic, and neither can fully represent the human condition. Not everyone is "hustling" 24/7, no matter how hard they'd like you to convince you that they are. On the other side of that coin, devolving into degeneracy and simply living a life of hedonism feels good for a couple of days, but ultimately leads to an unsatisfactory life as well.
One way to manage these opposing forces is to think about balancing work and play across a longer time period. What I mean by this, is that balance can be achieved across your entire lifetime instead of by the day. There might be a season in your life, say your late 20's where you end up putting in more grind and effort, but you can balance that out by relaxing more in your 30's as you reap the rewards of your actions in your 20's.
That's the kind of balance that I'm trying to achieve instead of worrying about balance in the short term. Let me know what you think about the concept of balance.
Tags:Article