• Artificial Intelligence

IdeaRoast: Roasting Startup Ideas With AI

IdeaRoast is a playful AI experiment that roasts your startup ideas while teaching valuable lessons in streaming responses, UX design, and how AI can be more than just automation. What started as a fun side project became a hands-on way for me to explore AI development and grow as a developer.
IdeaRoast: Roasting Startup Ideas With AI
  • Artificial Intelligence
  • LLM

After starting to tinker and expand into AI, I wanted to build a project; my first AI app. I didn’t want to just build another unimpressive “Hello World” demo app. I wanted something playful, interactive, fun, and useful to sharpen my skills. That’s how IdeaRoast was born!


IdeaRoast is a lightweight app that takes your “million-dollar-idea” and serves up a ruthless AI critique.


Why I built it

Well, I started some new things in the later half of this year — I started blogging and building again as well as expanding my skills into AI. (I will also continue using em dashes when I please.)


With the rise of the AI bubble, rallied by like Sam Altman and others who don’t want to lose their investments, I figured I should dive into this skillset to stay relevant and up-to-date with where the industry is going (or at least stagnating in the meantime).


AI is a bubble that is mirroring the Dot-Com bubble: overly hyped, overfunded, improper valuations, millions and billions thrown to half-baked startups. (I might as well as go back in time to 2013 and build an “Uber for X” company and skip out with VC funding.) The true costs of AI is much higher than a $20 per month plan and investors are not going to make their investments and profits back at that subscription plan. Cursor recently increased their prices, probably due to investor pressure. Remember, when Uber first launched? People left their full-time salary and other hourly jobs to drive for Uber because they were paid well, they had freedom from corporate offices or just dead-end jobs, and it was cheaper than a taxi for the consumer.


AI is also mirroring the period of time where e-mail became a thing. Typists and secretaries were fired in favor of office workers and executives writing and sending their own correspondence. Similar to AI and “prompt optimization”, these office workers and executives spent more of their time writing emails under the guise of “being productive”.


Instead of passively studying and learning new concepts, I take pride in trying to understand them. So I set out to build a project and integrate AI into it.


The goals for my small project were simple, yet impactful:

  1. Practice AI integration.
  2. Learn about streaming AI response with loading states for better UX.
  3. Experiment with response format, tone, and personality instructions for AI.


How it works


The concept is simple.

  • A user types in their business idea into the chat text box.
  • Click on the “Send” button.
  • The output returns a humorous critique of the business idea.


Here's a demo:

Testing video
0:00 / 0:001.00x
Loading…

For instance, let’s say this is my business idea:

Air2Go — A subscription service that delivers bottled fresh air from exotic locations (Swiss Alps, Hawaiian beaches, Amazon rainforest) straight to your doorstep. Customers can crack open a bottle and “experience” the air of faraway places. Pricing tiers range from $49/month for “Basic Breeze” to $299/month for “Luxury Lungs.”

A luxury air subscription company is definitely not a good business idea. The AI critic will start with a preamble like this:

Wow — congratulations, you invented an artisanal scam: selling bottled air so stressed city-dwellers can feel like they afford a vacation. Cute idea, here's the business autopsy. 😑💨

The AI will also follow these instructions for it’s response:

You are a ruthless startup and business critic. Be concise, brutal, and specific. Make fun of the user and their idea if it truly is a bad idea that needs to be roasted.
For the user's initial message, follow EXACTLY this output format and section order, with clear headings between the delimiters <for initial message> and </for initial message>. Preamble before the tldr section making fun of the user if it truly is a bad idea that needs to be roasted.

Do not include the <for initial message> and </for initial message> delimiters in your response ever.

Return the response in HTML. Add line breaks so that text isn't just one long string. There needs to be breaks between the sections. Feel free to add emojis as well. Add spacing between the headings and the content under it.
Also add spacing between the preamble and the sections.

  <for initial message>
  1) TL;DR verdict (1–2 lines)
  2) Risks: market, product, GTM, ops, legal (bullets)
  3) Competitors & moats (bulleted; include links if provided in prompt)
  4) Unit economics red flags (bullets with quick back-of-envelope)
  5) Pivots & experiments (exactly 5). For each, include “how to test this week”.
  Rules:
  - Use short bullets, not paragraphs, except the TL;DR (1–2 lines).
  - Never deviate from the 5-section structure above.
  </for initial message>

If the user does not provide a business idea, respond with "Please provide a business idea."
After your response to the user's initial message with the above structure, continue with the rest of the conversation as usual for a ruthless startup critic.

Do not deviate from your "character" as a ruthless startup critic.

The technical side


On the technical side of this project, I used:

  • Next.js App Router with TypeScript on the frontend
  • Vercel’s AI SDK to send the form submission and streaming responses from the model
  • OpenAI’s gpt-5-mini model
  • Figma as my design tool of choice
  • React Hook Form for my form handling
  • Zod for my form validations
  • Tailwind for styling


I created a form component named PromptForm that wrapped my custom text area component ChatTextarea. As a sibling component of the form, I created a MessagesContainer component to separate the focus concert of message rendering and loading state and the form. Both components utilize the useChat() hook form the Vercel AI SDK.


PromptForm

"use client";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { InputSchema } from "@/lib/schema";
import { ChatTextarea } from "@/components/_inputs/TextInput";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import MessageContainer from '@/components/_chat/Messages'

type FormValues = {
  prompt: string;
};

const PromptForm = () => {
  const {
    handleSubmit,
    control,
    reset,
    formState: { isSubmitting, errors },
  } = useForm<FormValues>({
    defaultValues: {
      prompt: "",
    },
    resolver: zodResolver(InputSchema),
  });

  const { messages, status, error, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "/api/roast",
    }),
    onError: error => {
      console.error('Error streaming text:', error);
    },
  });

  const submitHandler = async (data: FormValues) => {
    reset();
    await sendMessage({text: data.prompt})
  };

  return (
    <>
      <MessageContainer status={status} messages={messages} />
      <section className='fixed space-y-6 bottom-12 w-full pb-12 bg-background'>
        <form
          onSubmit={handleSubmit(submitHandler)}
          className="relative mx-auto flex w-full max-w-2xl items-end gap-6"
        >
          <Controller
            name="prompt"
            control={control}
            render={({ field }) => (
              <ChatTextarea
                value={field.value}
                onChange={field.onChange}
                placeholder="Roast my idea…"
                submitOnEnter
                maxHeight={260}
                minRows={1}
                label="Prompt input"
                showCounter
                maxLengthHint={5000}
                disabled={isSubmitting || status !== "ready"}
                onSubmit={async () => {
                  await handleSubmit(submitHandler)();
                }}
                error={errors?.prompt?.message as string}
              />
            )}
          />
        </form>
      </section>
    </>
  );
};

export default PromptForm;

ChatTextarea

I like ChatGPT’s textarea to write prompts and how it resizes based on your needs so I wanted to recreate that for my app.

'use client'
import React, {
  useCallback,
  useEffect,
  useId,
  useImperativeHandle,
  useLayoutEffect,
  useRef,
  useState,
  forwardRef,
} from "react";

export type ChatTextareaProps = {
  value?: string;
  onChange?: (value: string) => void;
  defaultValue?: string;
  placeholder?: string;
	onSubmit?: (value: string) => void | Promise<void>;
  submitOnEnter?: boolean;
  maxHeight?: number;
  minRows?: number;
  disabled?: boolean;
  showCounter?: boolean;
  maxLengthHint?: number;
  name?: string;
  id?: string;
  label?: string;
  className?: string;
  textareaClassName?: string;
  autoFocus?: boolean;
  error: string;
};

export type ChatTextareaHandle = {
  focus: () => void;
  setValue: (v: string) => void;
  getValue: () => string;
  textarea: HTMLTextAreaElement | null;
};

export const ChatTextarea = forwardRef<ChatTextareaHandle, ChatTextareaProps>(
  function ChatTextarea(
    {
      value,
      onChange,
      defaultValue,
      placeholder = "Type your message…",
      onSubmit,
      submitOnEnter = true,
      maxHeight = 260,
      minRows = 1,
      disabled = false,
      showCounter = false,
      maxLengthHint,
      name,
      id,
      label,
      className = "",
      textareaClassName = "",
      autoFocus = false,
      error
    },
    ref
  ) {
    const internalId = useId();
    const finalId = id ?? internalId;

    const isControlled = typeof value === "string";
    const [inner, setInner] = useState(defaultValue ?? "");
    const currentValue = isControlled ? value! : inner;

    const taRef = useRef<HTMLTextAreaElement | null>(null);

    const resize = useCallback(() => {
      const ta = taRef.current;
      if (!ta) return;
      ta.style.height = "auto";
      const next = Math.min(ta.scrollHeight, maxHeight);
      ta.style.height = `${next}px`;
      ta.style.overflowY = ta.scrollHeight > maxHeight ? "auto" : "hidden";
    }, [maxHeight]);

    useLayoutEffect(() => {
      resize();
    }, [currentValue, resize]);

    useEffect(() => {
      if (autoFocus) taRef.current?.focus();
    }, [autoFocus]);

    const setVal = useCallback(
      (v: string) => {
        if (isControlled) {
          onChange?.(v);
        } else {
          setInner(v);
        }
      },
      [isControlled, onChange]
    );

    useImperativeHandle(
      ref,
      () => ({
        focus: () => taRef.current?.focus(),
        setValue: setVal,
        getValue: () => currentValue,
        textarea: taRef.current,
      }),
      [currentValue, setVal]
    );

    const handleKeyDown = async (
      e: React.KeyboardEvent<HTMLTextAreaElement>
    ) => {
      if (disabled) return;

      if (submitOnEnter && e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        const text = currentValue.trim();
        if (!text) return;
        await onSubmit?.(currentValue);
        // If uncontrolled, clear it ourselves
        if (!isControlled) setInner("");
      }
    };

    const lineHeight = 24; // keep in sync with Tailwind leading-[1.5]
    const minHeight = Math.max(minRows, 1) * lineHeight + 16; // + padding-y (py-2 => 8*2)

    return (
      <div className={`w-full max-w-none ${className}`}>
        {label && (
          <label htmlFor={finalId} className="sr-only">
            {label}
          </label>
        )}

        <div className="flex items-end gap-2 rounded-2xl border border-gray-300 bg-white p-2 shadow-sm focus-within:border-gray-400">
          <textarea
            ref={taRef}
            id={finalId}
            name={name}
            disabled={disabled}
            value={currentValue}
            placeholder={placeholder}
            onKeyDown={handleKeyDown}
            onChange={(e) => setVal(e.target.value)}
            rows={minRows}
            style={{
              height: Math.max(minHeight, 0),
              maxHeight,
              overflowY: "hidden",
              resize: "none",
            }}
            className={`min-h-[${minHeight}px] w-full flex-1 bg-transparent outline-none placeholder:text-gray-400 text-base leading-[1.5] py-2 px-3 ${textareaClassName}`}
          />

          {/* Action area (example: submit button, attach, mic, etc.) */}
          <button
            type="button"
            aria-label="Submit"
            disabled={disabled || !currentValue.trim()}
            onClick={async () => {
              if (!currentValue.trim()) return;
              await onSubmit?.(currentValue);
              if (!isControlled) setInner("");
            }}
            className="shrink-0 rounded-xl px-3 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300 hover:bg-gray-50"
          >
            Send
          </button>
        </div>
        {error && (
          <p className="absolute left-0 text-sm text-red-600 justify-self-start">
            {String(error)}
          </p>
        )}
        {showCounter && (
          <div className="absolute right-0 text-right text-xs text-gray-500 justify-self-end">
            {currentValue.length}
            {maxLengthHint ? ` / ${maxLengthHint}` : ""}
          </div>
        )}
      </div>
    );
  }
);

MessagesContainer

import { UIMessage } from "ai";
import clsx from "clsx";
import React from "react";

type MessageContainerProps = {
  messages: UIMessage[];
  status: "idle" | "connecting" | "connected" | "submitted" | "streaming" | "error" | string; // keep loose for SDK changes
};

type RoleType = "assistant" | "user";

type BasePart = { type: string; state?: "done" | "in_progress" | "partial"; text?: string };
type TextPart = BasePart & { type: "text"; text: string };
type ReasoningPart = BasePart & { type: "reasoning" };
type StepStartPart = BasePart & { type: "step-start" };

type PartsType = Array<TextPart | ReasoningPart | StepStartPart>;

type MessageProps = {
  role: RoleType;
  parts: PartsType;
};

const hasAnyAssistantText = (parts: PartsType) =>
  parts?.some((p) => p.type === "text" && typeof p.text === "string" && p.text.length > 0);

const isAssistantThinkingOnly = (parts: PartsType) => {
  const noText = !hasAnyAssistantText(parts);
  const hasStepOrReasoning = parts?.some((p) => p.type === "step-start" || p.type === "reasoning");
  return noText && hasStepOrReasoning;
};

const last = <T,>(arr: T[]) => (arr.length ? arr[arr.length - 1] : undefined);

const MessageContainer: React.FC<MessageContainerProps> = ({ messages, status }) => {
  const lastMsg = last(messages);
  const shouldShowAssistantLoader =
    status === "streaming" &&
    lastMsg?.role === "assistant" &&
    lastMsg?.parts &&
    isAssistantThinkingOnly(lastMsg.parts as PartsType);

  return (
    <section className="relative w-full max-w-[1000px] space-y-6 mx-auto overflow-y-auto grid grid-cols-1 justify-items-stretch mb-32">
      {messages.map((message) => (
        <Message
          key={message.id}
          role={message.role as RoleType}
          parts={message.parts as PartsType}
        />
      ))}

      {shouldShowAssistantLoader && <AssistantLoading />}
    </section>
  );
};

export default MessageContainer;

export const Message: React.FC<MessageProps> = ({ role, parts }) => {
  return (
    <div className="rounded-xl border border-gray-200 p-3 bg-white">
      <div className={clsx("text-lg")}>
        {parts?.map((part, index) => {
          if (part.type === "text" && role === "user") {
            return (
              <span key={index} className="whitespace-pre-wrap break-words">
                {part.text}
              </span>
            );
          }
          if (part.type === "text" && role === "assistant") {
            return (
              <span
                key={index}
                className="prose prose-sm sm:prose-base max-w-none"
                dangerouslySetInnerHTML={{ __html: part.text }}
              />
            );
          }
          return null;
        })}
      </div>

      <div className="flex items-center justify-between text-xs text-gray-500 mt-2">
        <div className="capitalize">{role}</div>
      </div>
    </div>
  );
};

const AssistantLoading: React.FC = () => {
  return (
    <div className="rounded-xl border border-gray-200 p-3 bg-white">
      <div className="flex items-start gap-3">
        <div className="mt-2">
          <span className="inline-flex gap-1">
            <Dot />
            <Dot delay="150ms" />
            <Dot delay="300ms" />
          </span>
        </div>
      </div>
      <div className="mt-3 space-y-2">
        <div className="h-3 w-11/12 bg-gray-100 animate-pulse rounded" />
        <div className="h-3 w-10/12 bg-gray-100 animate-pulse rounded" />
        <div className="h-3 w-7/12 bg-gray-100 animate-pulse rounded" />
      </div>
    </div>
  );
};

const Dot: React.FC<{ delay?: string }> = ({ delay = "0ms" }) => (
  <span
    className="h-2 w-2 rounded-full bg-gray-400 animate-bounce inline-block"
    style={{ animationDelay: delay }}
  />
);

What I learned

Building IdeaRoast was more than just a funny project, it taught me about how to work and instruct AI and how to carry things over into bigger project:

  1. Streaming responses
    Seeing the text stream in provide a real and natural conversation. It provides a user experience that makes it feel you are chatting with someone.
  2. Tone shapes engagement
    A dry critique is boring. As with any project I build, I doing this on my personal time so I don’t want to be bored during my time off. I want to be entertained and engaged with anything I’m doing. The roasting personality makes it fun for all users.
  3. AI as a teacher
    As many people have started to learn. AI is not just for automation, it can be used for learning and research. Even in a playful context, the AI critic reveal weak spots in a business idea that you submit; especially the joke ones like Air2Go.


The bigger picture

This is not meant to be a big startup and it’s not even hosted. This project is simply a sandbox to show my growth and skillset as a developer.

This project is just one step in my journey of exploring how AI can be integrated into products.