DEV Community

Davide De Sio
Davide De Sio

Posted on

Build an AI powered new year's resolutions suggester ridiculously fast with Amplify AI KIT and Amazon Nova - Part 2

🔁 (Re)start from the beginning

As I mentioned in this earlier post, I've managed to quickly whip up a fun tool for the Eleva team: it was a chat powered with AI to generate New Year’s resolutions for 2025, and maybe drive my colleague Claudia a little crazy.

Of course, when you start tinkering, people get ideas. Enter Luca, who asked me to tweak it so we could turn this into a public tool. Most of his ideas were about gamifying the experience, super easy to implement.

But today, I want to talk about one of those tweaks as it is particularly relevant if you’re building solutions with the AWS Amplify AI KIT choosing Amazon Nova models: moving from a conversational UX to a generative one.

Luca’s vision? A simple tool without a chat-like interface. Instead, it should have a form-based UI where users enter simple data to generate their resolutions, complete with a generation of a LinkedIn-ready post they can copy and paste.

Sounds simple, right? Use “Generation” offered by Amplify AI Kit to create a generative endpoint, and display the data however you want.

Small catch: Generations only work with Anthropic models, and I wanted to experiment with Amazon’s shiny new Nova models.

🐇 Peek into the rabbit hole

What now?
The answer was classic: read the docs to see what actions the component was performing.

Amplify’s docs are pretty solid, and it didn’t take long to find this section on the request-response flow.

🔄 Request-response flow

The documentation walks you through the basics:

  • Create a chat programmatically
  • Subscribe to streaming responses
  • Send a message programmatically

Here’s the code snippet:

import { generateClient } from 'aws-amplify/data';
import { type Schema } from '../amplify/data/resource'

const client = generateClient<Schema>();

// 1. Create a conversation
const { data: chat, errors } = await client.conversations.chat.create();

// 2. Subscribe to assistant responses
const subscription = chat.onStreamEvent({
  next: (event) => {
    // handle assistant response stream events
    console.log(event);
  },
  error: (error) => {
    // handle errors
    console.error(error);
  },
});

// 3. Send a message to the conversation
const { data: message, errors } = await chat.sendMessage('Hello, world!');
Enter fullscreen mode Exit fullscreen mode

The key detail is that there are two types of response events:

  • ConversationStreamTextEvent: Partial text responses
  • ConversationStreamDoneAtIndexEvent: Signals the end of a response

🎨 Change my app front-end

With the backend sorted, I updated the UI for Luca’s request. The plan:

  • A control panel to set up the generator
  • Clickable “bullet” buttons for popular resolutions
  • Inputs for the user’s name and job title
  • A “cringe slider” (we all know why that’s needed 😅)
  • A shiny Generate button

Here’s the React code:

<div id="controls">
    <div>
        <div className="hint">
            Ciao! Sono il generatore di buoni propositi Eleva, il tuo assistente virtuale pronto a rendere i tuoi buoni propositi del {currentYear} ancora più straordinari!
            Dimmi un argomento, e con un pizzico di magia e tanta ispirazione, farò il resto. Ad esempio:
        </div>
        <div className="bullets">
            <button onClick={() => setArgument("non fare più di 3 call al giorno")} >non fare più di 3 call al giorno</button>
            <button onClick={() => setArgument("non far salire il cane sul divanoo")} >non far salire il cane sul divano</button>
            <button onClick={() => setArgument("scrivere un libro")} >scrivere un libro</button>
            <button onClick={() => setArgument("partecipare come speaker a un ted talk")} >partecipare come speaker a un ted talk</button>
            <button onClick={() => setArgument("imparare a ballare la polka")}>imparare a ballare la polka</button>
            <button onClick={() => setArgument("tornare a giocare a calcetto preservando i menischi")}>tornare a giocare a calcetto preservando i menischi</button>
            <button onClick={() => setArgument("usare 🐙meno 🐙 emoticon 🐙")}>usare 🐙meno 🐙 emoticon 🐙</button>
            <button onClick={() => setArgument("comprare camicie che non siano a quadri")} >comprare camicie che non siano a quadri</button>
            <button onClick={() => setArgument("eliminare whatsapp web dalle 9 alle 18")} >eliminare whatsapp web dalle 9 alle 18</button>
        </div>
        <div className="argument-container">
            <Input placeholder="Il tuo nome" value={userName} onChange={handleUserNameChange}></Input>
            <Input placeholder="Il tuo titolo di lavoro" value={userTitle} onChange={handleUserTitleChange}></Input>
            <TextAreaField
                value={argument}
                onChange={handleArgumentChange}
                placeholder="Scrivi qui il tuo argomento"
                label=""
            />
        </div>
    </div>

    <div className="range-container">
        <label>Cringe Meter: {cringeLevel}</label>
        <input
            id="cringeRange"
            type="range"
            min="0"
            max="100"
            value={cringeLevel}
            onChange={handleRangeChange}
        />
        <div>
            <span>No Cringe</span>
            <span>Super-Cringe</span>
        </div>
    </div>
    <button className="pushable" id="generate" onClick={handleClick}>
        <span className="front">
        Genera i tuoi nuovi propositi 🪄
        </span>
    </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Check out the final look:

Generation UI

Compared to the previous conversational one!

Old Conversational UI

🔗 Hooking up the backend

With the UI ready, I connected the handleClick function to the chat logic. On click:

  • Create a temporary chat
  • Send the user’s data to the AI
  • Stream the response back to the UI

Here’s the magic:

const handleClick = async () => {
    console.log("Click");
    setControlsVisible(false);
    setLoading(true);

    try {
        // 1. Create a conversation
        const { data: chat } = await client.conversations.chat.create();

        if (chat) {
            // 2. Subscribe to assistant responses
            chat.onStreamEvent({
                next: (event) => {
                    // Handle assistant response stream events
                    if (event.text) {
                        console.log("Acquiring text: " + event.text);
                        setChatLog((prev) => `${prev}${event.text}`);

                        // Animate opacity of the Post component
                        let opacity = 0;
                        const interval = setInterval(() => {
                            opacity += 0.1;
                            if (opacity >= 1) {
                                clearInterval(interval);
                                opacity = 1;
                            }
                            setPostOpacity(opacity);
                        }, 50); // Update every 50ms

                    }
                    if (event.contentBlockDoneAtIndex) {
                        setLoading(false);
                    }
                },
                error: (error) => {
                    // Handle errors
                    console.error(error);
                    setControlsVisible(true);
                    setLoading(false);
                },
            });

            // 3. Send a message to the conversation
            await chat.sendMessage(
                `Argomento: "${argument}". Cringe level: ${cringeLevel} su 100`
            );
        }
    } catch (error) {
        console.error("An error occurred:", error);
    }
};
Enter fullscreen mode Exit fullscreen mode

📝 Create the presentation component

Finally, I built the component to display the LinkedIn-ready result. Bonus: a copy-to-clipboard button for quick sharing.

import Markdown from "react-markdown";
import rehypeHighlight from "rehype-highlight";
import {useState} from "react";

const Post = ({
                  chatLog,
                  userName,
                  userTitle,
              }: {
    chatLog: string;
    userName: string;
    userTitle: string;
}) => {
    const [notification, setNotification] = useState<string | null>(null);
    const copyToClipboard = () => {
        const postContent = document.querySelector("#post-container .post-content");
        if (postContent) {
            const textToCopy = postContent.textContent || ""; // Get plain text content
            navigator.clipboard
                .writeText(textToCopy)
                .then(() => {
                    setNotification("Testo copiato, puoi incollarlo su LinkedIn");
                    setTimeout(() => setNotification(null), 3000); // Clear notification after 3 seconds
                })
                .catch((err) => {
                    console.error("Failed to copy text: ", err);
                });
        }
    };

    const prompt = "Una bella immagine per LinkedIn di nome " + userName + " e con ruolo " + userTitle;

    return (
        <div id="post-container">
            {notification && (
                <div
                    className="notification"
                >
                    {notification}
                </div>
            )}
            <div className="post">
                {/* Copy button */}
                <button
                    className="button-53"
                    onClick={copyToClipboard}
                >
                    Copia per LinkedIn
                </button>
                <div className="post-user-info">
                    <div className="post-user-icon">
                        <img src={"https://image.pollinations.ai/prompt/" + prompt + "?width=50&height=50&seed=` + uuid + `&nologo=true"} alt="User icon" />
                    </div>
                    <div>
                        <div className="post-user-name">{userName}</div>
                        <div className="post-user-title">{userTitle}</div>
                        <div className="post-time">2h • 🌍</div>
                    </div>
                </div>
                <div className="post-content">
                    <Markdown rehypePlugins={[rehypeHighlight]}>{chatLog}</Markdown>
                </div>
                <div className="post-engagement">
                    <div>👍 256 • 💬 48 • 🔄 12</div>
                    <div>15 comments</div>
                </div>
            </div>
        </div>
    );
};

export default Post;
Enter fullscreen mode Exit fullscreen mode

Here the result:

LinkedIn Post Generation

🎨 Shoutout to pollinations.ai

Did you notice? Even the user’s image in the LinkedIn post is AI-generated, thanks to pollinations.ai/.

I love this service. With a simple GET request and a prompt, you get an image tailored to your input. How cool is that? 🎉

Huge props to their team—fantastic work! You can find their project on GitHub here.

✨ Takeaways

It’s likely that AWS Amplify AI KIT and the Nova models will support “generation” routes in the near future. Until then, it was both fun and challenging to figure out an alternative solution while sticking to the Amplify framework.

Understanding what’s happening under the hood for each component opens the door to implementation possibilities beyond what’s officially offered. 🚀

🌐 Resources

You can find this project open sourced here on "develop" branch.

🙋 Who am I

I'm D. De Sio and I work as a Solution Architect and Dev Tech Lead in Eleva.
I'm currently (Apr 2024) an AWS Certified Solution Architect Professional, but also a User Group Leader (in Pavia) and, last but not least, a #serverless enthusiast.
My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.

Top comments (0)