In plain English
An AI writing assistant is a small app that takes text you already have and transforms it: it rewrites a clumsy paragraph, shortens a long email into three bullet points, or shifts a blunt message into a friendlier tone. You are not asking it an open question like a chatbot — you hand it a chunk of text plus a button ("Summarize", "Make it formal"), and it hands you a new version of that text.

Think of it like a kitchen blender with preset buttons. You drop in the same ingredients (your draft), but "smoothie" and "crushed ice" give very different results because each button runs a different program. In a writing assistant, each button — rewrite, summarize, change tone — is a different instruction sent to a large language model along with your text. The model does the work; your app just picks the right instruction and shows the result.
This is a great second project, right after a chatbot. A chatbot is open-ended — the user can type anything, and you manage a back-and-forth conversation. A writing assistant is the opposite shape: single-purpose and one-shot. There is no conversation history to track. Each click is one self-contained request — text in, transformed text out — which makes it simpler to build and a perfect way to learn how prompt design actually drives behavior.
Why it matters
Most real-world AI features in everyday software are transformations, not chat. The "Improve writing" button in your email client, the "Summarize" action in a notes app, the tone slider in a messaging tool — none of those are conversations. They are exactly the shape you build here. Learning this pattern teaches you the most common way AI shows up inside a product.
For a builder, three things make this worth your time:
- It teaches prompt design through contrast. When the only thing that changes between two buttons is the instruction, you see directly how wording steers the model. Tighten one sentence in the prompt and the output visibly improves. That feedback loop is the fastest way to build prompt intuition.
- It is genuinely useful. A focused rewrite/summarize/tone tool solves a real problem people have every day. You can ship it, show it to friends, and get honest feedback — unlike a toy demo.
- It scales into bigger things. The exact same input → instruction → output structure underlies content tools, code explainers, translation features, and data extractors. Master it once and you can add an AI feature to almost any existing app.
There is also a trap this project helps you avoid early: the one giant do-everything prompt. Beginners often write a single prompt that tries to handle rewriting, summarizing, and tone all at once, deciding internally what the user wanted. It works in the demo and falls apart in practice — the model guesses wrong about which task you meant. Building distinct, named tasks from day one is a habit that pays off in every AI feature you build later.
How it works
Every action in a writing assistant is one round trip to a model API. The user picks a task and (optionally) a tone, your app assembles the right system prompt for that task, sends it with the user's text, and renders the reply. There is no memory between clicks — each request stands completely on its own.
The system prompt is per-task, not global
The key design decision: keep a separate system prompt for each task, and pick one at request time based on which button the user clicked. The user's draft goes in the user message; the instruction goes in the system message. This separation is what lets you reuse the same code path for every task — only the system prompt changes.
Here is what those prompts actually look like. Notice they are short, specific, and tell the model exactly what to return — and, just as important, what not to do (no chit-chat, no "Here is your rewritten text" preamble).
export const TONES = ["friendly", "formal", "confident", "casual"] as const;
export type Tone = (typeof TONES)[number];
export const PROMPTS = {
rewrite:
"You are an editor. Rewrite the user's text to be clear, correct, and " +
"natural. Keep the original meaning and roughly the same length. " +
"Return ONLY the rewritten text — no preamble, no explanation, no quotes.",
summarize:
"You summarize text. Condense the user's text into its key points. " +
"If it is one paragraph, return 1-2 sentences; if longer, return 3-5 " +
"short bullet points. Return ONLY the summary.",
tone: (tone: Tone) =>
`You rewrite text in a different tone. Rewrite the user's text in a ` +
`${tone} tone. Keep the meaning and all facts unchanged. ` +
`Return ONLY the rewritten text — no preamble, no explanation.`,
} as const;
export function systemPromptFor(task: string, tone: Tone) {
if (task === "rewrite") return PROMPTS.rewrite;
if (task === "summarize") return PROMPTS.summarize;
if (task === "tone") return PROMPTS.tone(tone);
throw new Error(`Unknown task: ${task}`);
}Calling the model and streaming the result
Once you have the right system prompt, the API call is tiny. Streaming the response — printing tokens as they arrive instead of waiting for the whole reply — matters a lot here, because rewrites of long text can take several seconds and a frozen button feels broken. The snippet below uses the official Anthropic SDK and the current model claude-haiku-4-5, which is fast and cheap — a good fit for short, snappy transformations.
import Anthropic from "@anthropic-ai/sdk";
import { systemPromptFor, type Tone } from "./prompts";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY from the env
export async function transform(
task: string,
text: string,
tone: Tone,
onToken: (chunk: string) => void,
): Promise<void> {
const stream = client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 2048,
system: systemPromptFor(task, tone), // the per-task instruction
messages: [{ role: "user", content: text }], // the raw draft
});
stream.on("text", onToken); // fire each delta into the textarea
await stream.finalMessage(); // resolves when the reply is complete
}A minimal editor UI
The interface can be tiny: one text area, a row of task buttons, and (for the tone task) a small dropdown. The output streams back into an editable area so the user can keep tweaking. A common, friendly pattern is to show the result in a second box next to the original — so the user can compare and copy what they like — rather than overwriting their draft in place.
async function runTask(task: string, text: string, tone: string) {
const out = document.getElementById("output") as HTMLTextAreaElement;
out.value = ""; // clear the previous result
const res = await fetch("/api/transform", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ task, text, tone }),
});
// The server streams plain text chunks; append each as it arrives.
const reader = res.body!.getReader();
const decoder = new TextDecoder();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
out.value += decoder.decode(value, { stream: true });
}
}That is the entire app in spirit: a form that posts { task, text, tone } to one endpoint, and an output box that fills in live. Everything else — a copy button, an undo, saved drafts — is a nice extra, not a requirement. For more on what makes these interfaces feel good, see what makes good AI UX and designing for LLM latency.
Writing assistant vs chatbot
Because this is your second project, it helps to see exactly how its shape differs from a chatbot. They use the same API, but almost every design choice goes the other way.
- Single-purpose: rewrite / summarize / tone
- One-shot — each click is independent
- No conversation history to manage
- System prompt chosen per task
- User picks a button, not types a question
- Output replaces or sits beside the input
- Open-ended: user can ask anything
- Multi-turn — replies depend on prior turns
- Must send the whole history each time
- One system prompt for the whole session
- User types free-form messages
- Output is a new message in a thread
The big practical win is that no history means no chat-history management. You do not grow a messages array, you do not worry about the conversation getting too long, and you do not pay to re-send earlier turns. Each request carries just the system prompt plus the current draft. That alone removes a whole category of bugs that chatbots have to deal with.
The trade-off: with no conversation, the user cannot follow up with "make it shorter" the way they would in chat. You handle that with more buttons (a "Shorter" task, a "Try again" that re-runs) rather than a dialogue. Designing those discrete actions well is the core UX work of a writing tool.
Common pitfalls
A writing assistant is quick to get working and easy to get subtly wrong. Most of the rough edges come from prompt design and from forgetting that the model is non-deterministic.
- The one giant do-everything prompt. The single most common mistake. One prompt that tries to detect "did they want a rewrite or a summary?" will guess wrong. Keep one explicit prompt per task and let the button decide, not the model.
- Chatty preambles. Without instruction, models love to wrap output in "Sure! Here's your rewritten text:". That junk is awful in an editor. Every task prompt should end with something like Return ONLY the transformed text — no preamble, no quotes.
- The model rewrites the meaning. A tone change should not invent facts or drop details. Say so explicitly: Keep the meaning and all facts unchanged. Otherwise "make it confident" can quietly turn a maybe into a promise.
- No empty-input guard. If the text box is empty, don't call the API — you pay for a request and get nonsense back. Disable the buttons until there is text.
- Ignoring length and cost. Very long pasted documents can exceed limits or get expensive. Cap the input, or warn the user, rather than silently sending a 50-page paste.
- Assuming identical output every run. The same draft and button can yield slightly different rewrites each time. That is normal — design a "Try again" button rather than fighting it.
Going deeper
The rewrite/summarize/tone trio is the foundation. Once it works, the natural next steps all reuse the same input → instruction → output skeleton — you are just adding tasks and refining prompts.
More tasks, same pattern. Translation, "fix grammar only", "expand into a full paragraph", "turn these notes into an email", "explain this like I'm five" — each is just one more named system prompt and one more button. Resist the urge to merge them; a clear menu of focused tasks beats one clever prompt that tries to read minds.
Few-shot examples for stubborn tasks. If a task keeps producing the wrong style (too long, wrong format), put one or two example input/output pairs inside the system prompt. Showing the model a sample of exactly what you want is often more reliable than describing it in words — this is the cheapest way to lock in a consistent format.
Custom and saved tones. Let users define their own tone ("like my favorite newsletter") by feeding their description into the tone prompt template. Because the tone is just a string slotted into the prompt, supporting custom tones is almost free once the template exists.
Structured output when you need to parse it. If a task should return machine-readable data (say, a summary plus a list of action items as JSON), don't hope the model formats it right — use the provider's structured-output feature to force a schema. That turns "usually valid JSON" into "always valid JSON", which matters the moment another part of your app has to read it.
Picking a model and shipping it. Short transformations run great on a small, fast model; reach for a larger one only when quality on hard rewrites isn't good enough. When you are ready to put it online, the same deployment options that host a chatbot host this — it is a single server route plus a static page. The honest lesson of this project: the model is the easy part, already trained and one API call away. Your craft is in the prompts, the task menu, and the small UX details that make the tool feel effortless.
FAQ
How do I build an AI writing assistant from scratch?
Build it in three parts: a small UI with a text box and task buttons, a server route that holds your API key, and a set of system prompts — one per task. When a user clicks "Rewrite" or "Summarize", your server picks that task's system prompt, sends it to a model API along with the user's text, and streams the result back. Start with the three core tasks (rewrite, summarize, change tone) and add more later.
What is the difference between a writing assistant and a chatbot?
A chatbot is open-ended and multi-turn — the user types anything and each reply depends on the prior conversation, so you must track and re-send the whole history. A writing assistant is single-purpose and one-shot — the user picks a task button, you transform one chunk of text, and each click is completely independent. The writing assistant is simpler because there is no conversation history to manage.
Should I use one prompt for all tasks or a separate prompt per task?
Use a separate system prompt per task. A single "do everything" prompt forces the model to guess whether the user wanted a rewrite, a summary, or a tone change, and it guesses wrong often enough to feel unreliable. Let the button the user clicked decide which prompt to send — your code picks it, the model doesn't have to.
How do I stop the model from adding "Here is your rewritten text" before the output?
End each system prompt with an explicit instruction like "Return ONLY the transformed text — no preamble, no explanation, no quotes." Models add chatty wrappers by default; telling them exactly what to return (and what to leave out) removes the junk. If it still slips through occasionally, you can also strip a leading "Here is..." line in code as a backup.
Which model should I use for a writing assistant?
Short transformations — rewrites, summaries, tone shifts — run well on a small, fast, cheap model, which keeps responses snappy and costs low. Use a larger model only if quality on harder rewrites isn't good enough. Streaming the output matters more than raw model size here, because it makes even multi-second responses feel responsive.
Do I need to manage conversation history in a writing assistant?
No. Because each click is a one-shot transformation, every request carries only the system prompt and the current text — there is no history to store, grow, or re-send. This is a major reason a writing assistant is simpler to build than a chatbot, and it removes a whole class of context-length and cost issues that chatbots face.