In this chapter, we're going to implement text-to-speech history, as well as the general polish around this feature to make it feel complete. So the history tab will look something like this. Once the user selects history, we're going to go ahead and display all of the previous generations, including the text of the generation, a voice, and the time it was made. Clicking on each of these generations will redirect the user to the detail page with the generation ID. Besides that, we're also going to go ahead and implement some suggestions or prompts which users can click that are going to give them some fun texts to try.
We're also going to implement mobile responsiveness here because at the moment on mobile there is no way to change the settings or view history. So let's go ahead and start developing this. One thing I want to do before we start is go into Railway and show you one thing. So you remember that we added environment variables, right? Well, we did it incorrectly because we didn't do it in production environment.
So I would suggest that you go inside of your railway project, make sure you're looking at production and then in here go into variables and raw editor once again. You can see that the variables are missing because the ones we have added we have added to a temporary pull request environment. So again, remove all of them besides skip environment validation. And then just add the rest. Okay, so just make sure you have skip environment validation.
That one is important. And click update variables and click deploy up here in the corner and we will gonna see this at the end of the chapter. Alright let's go ahead and develop the history. The good news is that we already have the generations router and inside of here we already have get all. So all we have to do is build the UI.
So I want to go ahead and do the following. I'm going to go inside of the app dashboard text to speech page. In here we are currently only prefetching one thing, the voices get all, but we should also be prefetching all generations so we can load the history tab. Let's do the same thing inside of generation ID page.tsx. So in here we are prefetching an individual generation by ID, all voices, but we should also prefetch all other generations so we can load the history.
Great. Now that we have this, let's go ahead and modify our features, text to speech, components, settings panel, history right here. So we're going to change this by adding TRPC. So let's go ahead and import useTRPC. And then we're going to go ahead and use suspense query to load the TRPC generations get all because we are now prefetching them so we can suspense expect them and then we're gonna go ahead and render this exact thing that we have right here, no generations, only if generations.length doesn't exist.
So only then do this return. Otherwise, we're going to go ahead and do a different return. So let's go ahead and write that down here. In that case, we're gonna go ahead and write a div and let me just see what's going on. Something's incorrect here.
So if generations.length is not return this, otherwise return this, My apologies, outside of the curly bracket, outside of the if clause. And now in this flex flex call gap1 padding2, we're gonna go ahead and iterate over the generations. And then each generation will be a link element. So I'm gonna go over the class names in a second. Let's just ensure that you have imported link from next link.
The href will be forward text-to-speech generation.id. The key will be generation ID. Class name will be flex, items center, gap 3, rounded large, padding 3, text left, transition colors, and hover background muted. Flex-items-center, gap-3, rounded-large, padding-3, text-left, transition-colors, and hover-background-muted. Then let's go ahead and add a div with flex-minimum-width of 0, flex-1, flex-col, and gap-0.5.
And inside of that div, we're gonna go ahead and render a paragraph with truncate text small font medium and text foreground with generation dot text next to it we're gonna go ahead and add a div with a class name flex-items-center-gap-1.5-text-extra-small-text-muted-foreground. And in here, we're gonna go ahead and render a voice avatar. Let's go ahead and import voice avatar from components voice-avatar-voice-avatar. The seed will look for generation VoiceID and fallback to generation VoiceNameSnapshot similarly to what we already do in other components. And the name will be the voice name snapshot and class name shrink 0.
Then next to the voice avatar we're going to render the voice name snapshot then let's generate a dot using a unicode at mid dot semicolon and then let's go ahead and open one more span format distance to now which you can import from date fns and inside of here Let's go ahead and pass in the first argument which is new date generation created at and Then let's go ahead and simply add a suffix Like this add suffix true Great. So now let's go ahead and take a look at the history panel. So I'm going to go inside of text to speech here. And I'm going to click on history. And just like that, we should be able to get redirected into previous examples.
There we go. Beautiful. This is the SSL warning because of our database URL string. It's not related to our code. Beautiful.
So just like that we have implemented the history tab. Now let's go ahead and see what we have to do next. At the moment there is no way to view this on mobile. You can see I can only generate speech or listen but I can't open any of the history or settings. So that's what we're going to do now.
Inside of components let's go ahead and add history drawer.tsx and in here I'm going to import history from lucid react I'm gonna import button drawer content header title and trigger Then let's go ahead and import a fully completed settings panel history, which now properly loads the history elements. And let's go ahead and export function history drawer. And in here we're just going to create the mobile version of it in a nice drawer like view. So we're just going to do the normal composition of this components. So let's go ahead and render a drawer.
Let's go ahead and add a trigger. A trigger will have as child property and it's going to render a button. A button will have a variant of outline and the size of small and the button will have a history icon with class name size four. Then this trigger will open the following drawer content. The drawer content will have a header and finally a title of history.
Now outside of the header, we are very simply going to render the settings panel history within a div class name overflow-y-auto. Great! Now let's go ahead and implement the settings drawer. So we're gonna go ahead and we can maybe copy history drawer, paste it here and rename this to settings drawer. Let's go inside of settings drawer and we're gonna do a very similar thing.
Instead of importing history from Lucid React we're gonna import settings from Lucid React and let's go ahead and change the import of settings panel history to be settings panel settings from same name import. Now this one will be slightly different. So let's go ahead and add an interface settings drawer props optional open optional on open change and optional children. Then let's go ahead and change the name of this history drawer function to be settings drawer and in here we're gonna go ahead and let me just see the mistake I've made here. So export function settings drawer, which lacks return type annotation.
Okay, because I'm missing this. So open on open change and the children. And in here, we're gonna go ahead and turn this drawer into a controlled component. Okay? And we're going to render the drawer trigger only if we have children passed.
So this will be a multipurpose drawer. So this will be the same but it's just going to be using the settings. So we can use this kind of button but we can also do it in a different way. You will see why in a second. Let's change this to be the settings title and this will be settings panel settings.
All right, so slightly more complicated drawer but nothing too complicated. Then let's go ahead and create the voice selector button which is an alternative way to open the settings panel drawer. So inside of this components file, VoiceSelectorButton.tsx. Let's mark it as UseClient and import chevron down from lucid react and use store from 10 stack react form. Then let's go ahead and import button from components UI button.
Drawer trigger from components UI drawer. Voice avatar from components voice avatar voice avatar and use Typed App Form Context from hooks UseAppForm. Then let's go ahead and import UseTextToSpeechVoices from contexts TextToSpeechVoicesContext and TextToSpeechFormOptions from TextToSpeechForm. Let's export function VoiceSelectorButton and in here, let's grab all voices from use text speech voices. Let's go ahead and create the form using use type app form context and passing along the form options.
And let's go ahead and pick a currently active voice ID. So we are treating this as a form element because it's going to basically display here like the currently selected voice. And we need to treat it as a form element. It's essentially the same thing as this, right? This changes depending on what we select.
It's a form element. Yes, it can be pre-filled, but it's also a form element. So that's what we're doing now, but for mobile mode, okay? So the current voice is iteration of all voices well I can collapse this so you can see allvoices.find and we find the matching ID from the store otherwise we fall back to the top of the array. And let's go ahead and render a button label.
If we have current voice.name, we render that, otherwise select a voice. Great. Now let's go ahead and return the UI. We're going to add a drawer trigger as child because it's going to be used for that. Let's render a button with variant outline, size small and class name flex1, justify center, gap2 and pixel2, I mean px2, my apologies.
If we have the current voice selected in that case we're going to render a voice avatar with current voice ID and current voice name this is a form element so in here we don't have to do any fallbacks then outside of it let's go ahead and render the button label within a span with flex1, truncate, text-left, text-small and font-medium. And last but not least, let's go ahead and render a chevron down element, size 4, shrink 0 and text-muted foreground. Now we're going to go ahead inside of text input panel component in text to speech and in here we're going to add the import for settings drawer, we're going to add the import for history drawer and we're going to add the import for history drawer and we're going to add the import for voice selector button okay so make sure you have all of those and then let's go ahead and find the mobile layout here it is And then above the generate button here, we're gonna go ahead and render the following div flex item center and gap to and then we're going to render a settings drawer that is opened by clicking on the voice selector button and then beneath it we're going to render history drawer and this is what that's gonna look like so from here users can open settings on mobile and they should be able to open history.
They can. Okay, looks like it was a glitch. Perfect. And on desktop you can see none of that exists. So we have now officially done both.
Users can do the same thing on mobile that they can do on desktop. I just want to do a few tests so I should be able to change the voice and it immediately changes perfect and if I use history to switch to a manual there we go that changes to perfect so it works as expected. Now let's go ahead and build prompt suggestions for desktop scenario. So I'm going to go inside of source features text to speech components and in here I'm going to add prompt suggestions.tsx. Prompt suggestions are mostly a matter of copyright, I mean, yes, copywriting, like marketing stuff.
So feel free to open my source code and look at this file because I will be adding a bunch of text info which doesn't make sense to, you know, pause and copy when you can pause and write when you can just copy. So to build this component, We're going to need book, open, smile, microphone, languages, clapperboard, gamepad, tube, podcast, and brain from Lucid React. Then we're going to import badge from components UI badge. We're going to import the type Lucid Icon from Lucid React. And here comes the part where we have to build the prompt suggestions.
So prompt suggestions are going to be an array of objects. Each object will have a label, a prompt and an icon. And this will look like this, for example. Narrate a story, prompt and then a text and icon book open. And these will actually be very similar to quick actions.
We have something called quick actions in source, features, dashboard, data. And The same way you copied them from source code here. You can copy from source code here So it's located inside of source app features text-to-speech components prompt suggestions So I'm gonna go ahead and add all of the other ones. There we go. All of these.
You can of course choose how many of them you want to have. And now that you have that, let's go ahead and build the prompt suggestions function. So function prompt suggestions will accept on select with a prompt string which returns a void. And let's go ahead and return. In here we're gonna go ahead and render a div with class name space y 2.5.
We're going to render a paragraph which says get started with and then we're going to list over the suggestions. So this was text small text needed for ground. This is flex flex wrap gap 2 And we're going to go ahead and iterate over the prompt suggestions. So for each suggestion we're going to render a badge element with key suggestion label, variant outline, class name, cursor pointer, Gap 1.5, PY1, PX 2.5, Text ExtraSmall, Hover, BackgroundAccent, and RoundedMedium. On click we're going to call on select and suggestion.prompt.
And then we're going to render the icon which you can access through suggestion.prop.icon, give it a class name of size 3-0 and next to it we're going to render a label. Great. Now let's go ahead inside of the text input panel once again. And this time we are aiming for the desktop layout here. And let's go ahead and find where it says get started by typing or pasting the text above.
And instead of that paragraph, we're going to render the prompt suggestions. So let's go ahead and import prompt suggestions. And we have to add onSelect. So when user selects a prompt, what we want to do is we want to call form setFieldValue and modify the text field by adding the prompt value. So let's go ahead and click on text to speech so we have a completely empty text and this time instead of that blank text you should see all of your prompt suggestions.
If you start typing we get rid of them. But before you have anything you can select them. I think this is a very nice touch and it looks much more polished here. One last thing to do is to add a loading skeleton. So you can't really see, but it takes a while to load the text to speech page.
You can see that when I click on it, it's not instant. There's like a millisecond I have to wait when I click on text to speech, especially if I clear cache and then click. So one way you can improve that is by adding a loading file in Next.js. So this is happening because inside of dashboard text-to-speech page, you can see that text-to-speech view is immediately calling useSuspenseQuery, meaning that we are blocked from seeing this component being rendered until Generations getAll and Voices getAll are loaded. So because of that, let's go ahead and create a text to speech loading.tsx which is a reserved file name.
And in here you basically write your very own loading skeleton. So for example a paragraph which says loading like this and you can see that now clicking on this page from the navigation bar is instant right but the problem is It has this very brief loading state. So what I suggest you do, since there's not much knowledge in learning how to build loading pages, in fact you can do most of it with AI now, you just tell it to look at the page and say build me an equivalent loading, I would suggest that you just copy loading pages from my source code. We're going to do that by using the skeleton component and we are basically going to replicate all of the components we can see in the three layout panel. So this is the entire file.
Again, you don't need to have this file. It works fine without it. It just makes it a little bit more polished. So we're using the skeleton from Components UI skeleton. And I'm reusing the voice preview placeholder because it is a placeholder.
It's technically a skeleton. Right? And I'm just replicating, I mean AI replicated what the three panel layout looks like. We are replicating this, this, this. You can see that if I try and refresh this is what it looks like.
And you can see how good this looks so it's not everything in skeleton and now clicking on text to speech is instant and it has a very smooth skeleton again you don't have to do this if you don't want to. Brilliant! So Let's go ahead and confirm npm run lint works, npm run build works. That Now let's go ahead and merge this pull request. Since this was a super simple one, we really don't have to wait for a review because it was mostly just UI components, a skeleton and a few prefetches here and there.
We are aware of mostly all of the changes. No new business logic was introduced here. So we don't have to wait for the review in this case. We can just go ahead and merge the pull request anyway. And you will still see the most up-to-date on your railway because once you have merged to main this will trigger another redeployment on the railway so if you go there you should see your new project being built right now.
There we go, it's building. So you will see all the changes up to date. Brilliant. Now let's go ahead and do git checkout main and git pull origin main. And what I like to do as always is just double check that everything is ok so I'm on the main branch and inside of my graph I can see 07 history and polish being merged back into the main branch.
Brilliant! Amazing job and see you in the next chapter.