Posted on: 2023/08/29

In our team, while working on a prototype, we wanted to experiment with Atomic Design in frontend development. If you're scratching your head thinking, "What's Atomic Design?", let me break it down for you: Atomic Design is a methodology that breaks down interface design into five hierarchical stages:

  1. Atoms: These are the smallest building blocks like buttons, input fields, or colors.
  2. Molecules: Assemblies of atoms functioning as a unit, like a search bar combining an input field and a button.
  3. Organisms: Groups of molecules joined together to form a distinct section of an interface, like a header.
  4. Templates: These are page-level objects that place components into a layout but devoid of any real content.
  5. Pages: The highest level that focuses on content, variations, and data connections within the template.

Why is Atomic Design even good? Well, it helps you create scalable and maintainable design systems. Think of it like Lego blocks; each piece (or stage) is essential and serves a purpose. It makes component management easier, keeps your team on the same page, and most importantly, makes it simple to identify reusable components. πŸ™Œ

But, here's where we hit a snag. We kept asking ourselves, "Where does component 'x' go? Is it an atom or a molecule?" That's when it hit meβ€”why not consult ChatGPT? Maybe it can streamline this categorization for us. πŸ˜„

So, I built an app that sends a prompt to ChatGPT, parses the response, and displays the category to the user. Simple, right? The app is now live and you can check it out at https://atomic.aien.me. But if you're keen on the nitty-gritty details of how it was implemented, stick around. I'm going to walk you through it, step-by-step.

Setting Up the Basics πŸ“

Before diving into the code, let's make sure you've got the basics covered:

Install Dependencies 🀝

Start by installing Axios for making HTTP requests:

$ yarn add --dev --exact prettier
$ yarn add --dev eslint-config-prettier

And change the .eslintrc.json accordingly:

{
  "extends": ["next", "prettier"]
}

You must also add prettier script to your package.json file:

...
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "format": "prettier . --write"
  },
...

Directory Structure πŸ“‚

Before we dive into the code, let's talk about how I've laid out my directories and files. While you can structure yours in a way that suits you, here's a guide to what I've done:

β”œβ”€β”€ .env                      # Environment variables
β”œβ”€β”€ .env.local                # Local environment overrides
β”œβ”€β”€ .eslintrc.json            # ESLint configuration
β”œβ”€β”€ .gitignore                # Git ignore rules
β”œβ”€β”€ .prettierignore           # Files to ignore during prettification
β”œβ”€β”€ .prettierrc.json          # Prettier configuration
β”œβ”€β”€ README.md                 # Project README
β”œβ”€β”€ next-env.d.ts             # Next.js environment types
β”œβ”€β”€ next.config.js            # Next.js configuration
β”œβ”€β”€ package-lock.json         # npm dependency versions
β”œβ”€β”€ package.json              # npm package and script info
β”œβ”€β”€ postcss.config.js         # PostCSS configuration
β”œβ”€β”€ src                       # Source code folder
β”‚   β”œβ”€β”€ app                   # App specific components and logic
β”‚   β”‚   β”œβ”€β”€ api               # Backend API logic
β”‚   β”‚   β”‚   └── chat          # Chat API routes
β”‚   β”‚   β”‚       └── route.ts  # Route handling for chat
β”‚   β”‚   β”œβ”€β”€ globals.css       # Global styles
β”‚   β”‚   β”œβ”€β”€ layout.tsx        # App layout component
β”‚   β”‚   └── page.tsx          # Main page component
β”‚   └── types                 # TypeScript types
β”‚       └── index.ts          # Main type definitions
β”œβ”€β”€ tailwind.config.ts        # Tailwind CSS config
β”œβ”€β”€ tsconfig.json             # TypeScript config
└── yarn.lock                 # Yarn lock file

Key Points:

  • .env and .env.local: These files hold your environment variables. The .local version is for your local dev setup and typically not committed to version control.
  • src/app/api/chat/route.ts: This is where our ChatGPT API handler lives. It receives a UI component name, sends it to ChatGPT, and returns the Atomic Design category.
  • src/app/globals.css: Any global CSS styles go here. Keeps things neat.
  • src/types/index.ts: This is where I define any TypeScript types that I'll be using throughout the app.

Feel free to adapt this directory structure to fit your project's needs. A well-organized folder structure makes the development process much more pleasant, trust me. πŸ€“

Creating the Next.js API Handler πŸ”€

The cornerstone of this application is the Next.js API handler. This function serves as the intermediary that receives a component name, queries it against the ChatGPT model, and subsequently returns the appropriate Atomic Design categorization for the specified component.

import { ChatApiResponse } from "@/types";
import { NextRequest, NextResponse } from "next/server";

export async function POST(
	request: NextRequest,
): Promise<NextResponse<ChatApiResponse>> {
	const { input } = await request.json();

	if (!input) return NextResponse.json({ error: "error, input not defined" });

	const chatGPTApiUrl = "https://api.openai.com/v1/chat/completions";
	const apiKey = process.env.API_KEY;
	const model = "gpt-3.5-turbo";

	const payload = {
		messages: [
			{
				role: "user",
				content: "I will put this a bit later",
			},
		],
		model,
	};

	const headers = {
		Authorization: `Bearer ${apiKey}`,
		"Content-Type": "application/json",
	};

	try {
		const response = await fetch(chatGPTApiUrl, {
			method: "post",
			body: JSON.stringify(payload),
			headers,
		}).then((r) => r.json());

		return NextResponse.json({
			output: JSON.parse(response.choices[0].message.content),
		});
	} catch (e: any) {
		return NextResponse.json({ error: "error, input not defined" });
	}
}

Let's break down this code snippet piece by piece in a manner that's easy to understand.

Import Statements

import { ChatApiResponse } from "@/types";
import { NextRequest, NextResponse } from "next/server";
  • ChatApiResponse: This is a TypeScript type we've defined to structure the API response.
  • NextRequest, NextResponse: These are imported from Next.js and are used to type our API request and response.

The POST Function βš™οΈ

export async function POST(
	request: NextRequest,
): Promise<NextResponse<ChatApiResponse>> {

We define an asynchronous function named POST that takes a NextRequest object and returns a Promise with a NextResponse of type ChatApiResponse.

Parsing the Request πŸ”€

const { input } = await request.json();

We destructure input from the incoming JSON request payload.

Error Handling ‼️

if (!input) return NextResponse.json({ error: "error, input not defined" });

If input is not defined, we return an error response.

ChatGPT API Setup πŸ“

const chatGPTApiUrl = "https://api.openai.com/v1/chat/completions";
const apiKey = process.env.API_KEY;
const model = "gpt-3.5-turbo";

Here we define the URL for the ChatGPT API, the API key from environment variables, and the model we want to use.

Payload and Headers

const payload = { /* ... */ };
const headers = { /* ... */ };

payload contains the message that will be sent to ChatGPT. headers contain the authorization and content type information for the API request.

API Request and Response Handling βš™οΈ

try {
	// API request code here
	return NextResponse.json({
		output: JSON.parse(response.choices[0].message.content),
	});
} catch (e: any) {
	return NextResponse.json({ error: "error, input not defined" });
}

We use a try-catch block to handle the API request and response. The try block sends a request to ChatGPT and returns the parsed output. The catch block returns an error response if something goes wrong.

Talking to ChatGPT πŸ—£οΈ

First, I used a simple prompt to ask ChatGPT to categorize the component:

Using Atomic Design Principles, categorize the following UI component into one of the Atomic Design terms: Atom, Molecule, Organism, Template, Page, Unknown. The component name is: [component_name_here]

However, it didn't actually gave me enough information. So after experimenting a bit with the prompt, here's what I ended up with which I set for the payload.messages[0].content:

Using Atomic Design Principles, categorize the following UI component into one of the Atomic Design terms:
 - Atom πŸ§ͺ
 - Molecule βš—οΈ
 - Organism πŸ‹
 - Template πŸ“‘
 - Page πŸ“„
 - Unknown ❓.
The component name is: "${input}".
The output must always be a json with following format, at any cost:
{
  type: [the type],
  component: [what is the purpose of ${input} in UI. Why is it categorised as it is.]
  items: [an string array of possible components that can together make ${input} including their type in paranthesis in the format of "component name (component type)"]
}
In case of errors or that the input anything other than a component, write the single text "The component '${input}' does not fit into any of the Atomic Design terms."

Type checking 🦺

After establishing the prompt and what the response should look like, the next step is to define a TypeScript interface for the ChatGPT API response. Here's how:

export interface ChatApiResponse {
  output?: {
    type: string;
    component: string;
    items: string[];
  };
  error?: string;
}

Let me break it down for you:

  • output: This optional field houses the categorized UI component's details.
    • type: Indicates which Atomic Design category the UI component falls under.
    • component: Gives a quick description of why the UI component is categorized the way it is.
    • items: This is an array of strings, indicating the components that can collectively create the UI component in question. Each item also shows the type of component in parentheses.
  • error: An optional string field that will hold any error messages, if they arise.

This TypeScript interface ensures that any response from the ChatGPT API fits into a predefined structure, which can be easily manipulated or displayed within your Next.js components. πŸ€“

Absolutely, let's hop right into the UI development using React. 🎨

Crafting the UI with React πŸ› οΈ

Our user interface is built with React and TypeScript. The purpose is to create an interactive UI where a user can input a component name and get its Atomic Design category. Let’s break down what's happening in the code.

The Imports and State Management 🧳

First, we're importing essential modules and types:

"use client";
import { ChatApiResponse } from "@/types";
import { FormEvent, useState } from "react";
  • ChatApiResponse: Our own type definition to make sure the server response matches what we expect.
  • FormEvent, useState: Hooks and events from React to manage our form and state.

State Variables 🚦

We have two state variables:

  • response to store the server's answer.
  • loading to toggle a loading state.
const [response, setResponse] = useState<ChatApiResponse>();
const [loading, setLoading] = useState<boolean>(false);

Handling Form Submission πŸ“

We define an async function handleSubmit that gets triggered when the form is submitted.

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
  // ... the function's body
};

It fetches the data from our Next.js API and sets the response into state variables. Additionally, it toggles the loading state.

The JSX Structure πŸ—οΈ

Finally, we render our form and display the response.

return (
  <main className="grid h-screen place-items-center">
    {/* ... rest of the JSX */}
  </main>
);

Form πŸ“‹

The form comes with an input field for the component name and a submit button. On submission, it triggers the handleSubmit function.

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  const data = new FormData(e.currentTarget);
  const input = data.get("input");

  setLoading(true);

  const r: ChatApiResponse = await fetch("/api/chat", {
    method: "post",
    body: JSON.stringify({
      input,
    }),
  }).then((r) => r.json());

  setResponse(r);
  setLoading(false);
}

Response Display πŸ–₯️

We display the Atomic Design type, its purpose, and the components that can form it, all conditionally rendered based on whether data has been loaded or not.

{loading ? (
  <p className="mt-5 text-center">Loading... please wait</p>
) : (
  <>
    {/* ... the display logic for response */}
  </>
)}

And that wraps up our user interface! With this setup, you have a solid foundation for categorizing UI components according to Atomic Design principles. 🌐

The response part

<>
  <p className="mt-5 text-center">
    {response?.output?.type || 'Write "button" as an example.'}
  </p>
  <p className="mt-2">{response?.output?.component}</p>
  <p className="mt-2">
    {(response?.output?.items.length &&
      response?.output?.items.length > 0 &&
        `It is usually made of:`) ||
    ""}
  </p>
  <ul className="list-disc p-5">
    {response?.output?.items.map((i, idx) => (
      <li key={idx}>{i}</li>
    ))}
  </ul>
</>

Feel free to adjust the UI as per your brand's design guidelines or functional requirements. πŸŽ‰

Testing the App πŸ§ͺ

Running some tests proved that the app works like a charm! I sent in a variety of UI components and ChatGPT categorized them perfectly.

Absolutely, let's seal the deal with a compelling conclusion! 🎯

Wrapping Up 🌟

Building this ChatGPT-powered app was not just a coding exercise; it was a doorway into how we can blend the realms of design and automation. It’s a tiny cog in the machine of design innovation, but its impact is sizable when it comes to expediting the design process. πŸ•’

The atomic design classification issue we tackled is just the tip of the iceberg. There's a vast sea of possibilities when you think about what else could be automated or enhanced through machine learning and natural language processing. πŸŒŠπŸ’‘

What's Next? πŸ›£οΈ

Going forward, I plan to add more features, such as batch processing of components and integrating more design frameworks. And who knows? Maybe soon, we'll have a full-blown design assistant, courtesy of ChatGPT and some React magic. πŸ§™β€β™‚οΈπŸ’«

Thank You for Stopping By! πŸ‘‹

I truly hope you found this walk-through enlightening and that it sparked some ideas for you to explore. If you integrate ChatGPT into your own projects, I'd love to hear how it goes! Feel free to reach out and share your experiences. πŸ’Œ

Don't forget to check out the app at https://atomic.aien.me and let the atomization begin! πŸš€

Until next time, keep coding, keep designing, and keep pushing the boundaries of what's possible. πŸ’ͺ🎨

Cheers to bridging the gap between design and code! πŸ₯‚