DEV Community

Alex Chen
Alex Chen

Posted on

I Built an AI Writing Assistant in a Weekend

I Built an AI Writing Assistant in a Weekend

How I built a tool that helps me write 10x faster — and how you can too.

The Problem

I write a lot. Blog posts, documentation, client emails, proposals.

The problem isn't what to write — it's getting started and staying consistent:

  • Blank page paralysis
  • Inconsistent tone across documents
  • Spending 30 minutes on an email that should take 3
  • Forgetting key points mid-draft

What I Built

A simple web app that:

  1. Takes a topic/outline → generates a draft
  2. Takes my rough notes → polishes into clean prose
  3. Takes a finished piece → checks for consistency
  4. Maintains my writing style across all output

Tech stack: Node.js + Express + OpenAI API (could use any LLM)

The Architecture

Browser (textarea)
    ↓ POST /api/write
Express Server
    ↓ 
[Pre-processor] Clean input, extract intent
    ↓
[Prompt Builder] Assemble context-aware prompt
    ↓
[LLM Client] Call OpenAI/Claude/Gemini API
    ↓
[Post-processor] Format output, add metadata
    ↓
Response: { text, tokensUsed, suggestions }
Enter fullscreen mode Exit fullscreen mode

Core Code

// server.js
const express = require('express');
const OpenAI = require('openai');

const app = express();
app.use(express.json({ limit: '50kb' }));

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// My "style profile" — this is what makes output consistent
const STYLE_PROFILE = {
  tone: 'conversational but professional',
  voice: 'first-person singular, active voice',
  rules: [
    'Use short sentences (average <15 words)',
    'Avoid jargon unless explaining it',
    'Use concrete examples over abstract statements',
    'Start with the main point, then explain',
    'One idea per paragraph',
    'No filler phrases like "in todays world" or "it is important to note"',
  ],
  examples: `
    Good: "SQLite is faster for reads than PostgreSQL because it has no network overhead."
    Bad: "In today's fast-paced development environment, it's important to consider database performance implications."
  `,
};

function buildPrompt(input, mode) {
  const prompts = {
    draft: `You are a writing assistant. Write a draft about: "${input}"

Style guidelines:
- ${STYLE_PROFILE.tone}
- ${STYLE_PROFILE.voice}
${STYLE_PROFILE.rules.map(r => `- ${r}`).join('\n')}

Example of desired style:
${STYLE_PROFILE.examples}

Output ONLY the article content. No meta-commentary.`,

    polish: `Polish this text while keeping the original meaning:

"${input}"

Apply these style rules:
${STYLE_PROFILE.rules.join('\n')}

Return only the polished version.`,

    expand: `I wrote this outline/notes. Expand it into a full article:

"${input}"

Maintain this style: ${STYLE_PROFILE.tone}
Voice: ${STYLE_PROFILE.voice}
${STYLE_PROFILE.rules.map(r => `- ${r}`).join('\n')}`,
  };

  return prompts[mode] || prompts.draft;
}

app.post('/api/write', async (req, res, next) => {
  try {
    const { input, mode = 'draft' } = req.body;

    if (!input || input.trim().length < 10) {
      return res.status(400).json({
        error: 'Input must be at least 10 characters'
      });
    }

    const prompt = buildPrompt(input, mode);

    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini', // Cost-effective for writing tasks
      messages: [{ role: 'user', content: prompt }],
      temperature: 0.7,       // Some creativity but not random
      max_tokens: 2000,
    });

    const text = response.choices[0].message.content;
    const tokens = response.usage.total_tokens;

    // Estimate cost (gpt-4o-mini pricing)
    const costUSD = (tokens / 1_000_000) * 0.15; // ~$0.15 per 1M tokens

    res.json({
      data: { text, tokens, costUSD },
      meta: { model: 'gpt-4o-mini', mode }
    });

  } catch (err) {
    next(err);
  }
});

// Error handler
app.use((err, req, res, _next) => {
  console.error('[WRITE ERR]', err.message);
  res.status(err.status || 500).json({
    error: err.message || 'Writing service unavailable'
  });
});

app.listen(3001, () => console.log('Writing assistant on :3001'));
Enter fullscreen mode Exit fullscreen mode

The Frontend (Single Page)

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>AI Writing Assistant</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui; padding: 2rem; max-width: 800px; margin: auto; background: #fafafa; }
    h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #333; }
    .controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
    button { padding: 0.5rem 1rem; border: 1px solid #ccc; background: white; cursor: pointer; border-radius: 6px; }
    button.active { background: #333; color: white; border-color: #333; }
    textarea { width: 100%; min-height: 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; line-height: 1.6; resize: vertical; }
    .output { margin-top: 1rem; padding: 1rem; background: white; border: 1px solid #ddd; border-radius: 8px; min-height: 200px; white-space: pre-wrap; line-height: 1.6; }
    .meta { display: flex; gap: 1rem; margin-top: 0.5rem; font-size: 12px; color: #888; }
    .loading { opacity: 0.5; pointer-events: none; }
    @media (prefers-color-scheme: dark) { body { background: #1a1a1a; color: #e0e0e0; } textarea, .output { background: #2a2a2a; border-color: #444; color: #e0e0e0; } }
  </style>
</head>
<body>
  <h1>✍️ AI Writing Assistant</h1>

  <div class="controls">
    <button onclick="setMode('draft')" id="btn-draft" class="active">Draft from Topic</button>
    <button onclick="setMode('polish')" id="btn-polish">Polish Text</button>
    <button onclick="setMode('expand')" id="btn-expand">Expand Notes</button>
  </div>

  <textarea id="input" placeholder="Enter your topic, rough text, or notes..."></textarea>

  <div style="margin-top:0.5rem">
    <button onclick="generate()" id="btn-generate">Generate ✨</button>
    <button onclick="copyOutput()" id="btn-copy" style="display:none">Copy 📋</button>
  </div>

  <div class="output" id="output"></div>
  <div class="meta" id="meta"></div>

<script>
let currentMode = 'draft';

function setMode(mode) {
  currentMode = mode;
  document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
  document.getElementById('btn-' + mode).classList.add('active');
}

async function generate() {
  const input = document.getElementById('input').value.trim();
  if (!input) return;

  const btn = document.getElementById('btn-generate');
  btn.textContent = 'Generating...';
  btn.disabled = true;
  document.getElementById('output').textContent = '';
  document.getElementById('meta').textContent = '';

  try {
    const res = await fetch('/api/write', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input, mode: currentMode }),
    });

    const data = await res.json();

    if (data.error) throw new Error(data.error);

    document.getElementById('output').textContent = data.data.text;
    document.getElementById('meta').textContent = 
      `${data.data.tokens} tokens | ~$${(data.data.costUSD * 1000).toFixed(2)}¢`;
    document.getElementById('btn-copy').style.display = '';

  } catch (err) {
    document.getElementById('output').textContent = 'Error: ' + err.message;
  } finally {
    btn.textContent = 'Generate ✨';
    btn.disabled = false;
  }
}

function copyOutput() {
  navigator.clipboard.writeText(document.getElementById('output').textContent);
  const btn = document.getElementById('btn-copy');
  btn.textContent = 'Copied!';
  setTimeout(() => btn.textContent = 'Copy 📋', 1500);
}
</script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Usage Examples

Mode 1: Draft from Topic

Input: "Why developers should learn Docker in 2026"

Output: Full blog post (~800 words) with intro, 
        key points, code examples, conclusion
Enter fullscreen mode Exit fullscreen mode

Mode 2: Polish Text

Input: "so basically docker is like a box u put ur code in 
        and it runs everywhere same way its cool bc no more 
        'works on my machine' problems lol"

Output: "Docker packages your application and dependencies 
        into a container that runs identically across any 
        environment. This eliminates the classic 'works on 
        my machine' problem — your code behaves the same 
        way on development, staging, and production."
Enter fullscreen mode Exit fullscreen mode

Mode 3: Expand Notes

Input: "- Docker vs VMs
        - lighter weight
        - shared kernel
        - seconds to start
        - Dockerfile example
        - multi-stage builds"

Output: Full article with each point expanded into 
        paragraphs with explanations and code samples
Enter fullscreen mode Exit fullscreen mode

What It Costs Me

Feature Cost
gpt-4o-mini $0.15 per 1M tokens
Average article ~$0.003 (yes, less than half a cent!)
VPS hosting Already paying ($5/mo includes everything)
Domain Already have
Total per month ~$5.03

I spend less than 1 cent per generated article.

What I Learned Building This

  1. Prompt engineering IS product design. The quality of output depends entirely on how you frame the request. Invest time here.

  2. Style profiles are game-changers. Defining my writing rules once means consistent output forever. No more "rewrite this to sound like me."

  3. Mode switching beats one-size-fits-all. Having separate modes for drafting/polishing/expanding gives much better results than a single "make this better" prompt.

  4. Show token cost. Users trust transparent pricing. Showing "~0.3¢" makes them feel good about using the tool.

  5. Keep it simple. This entire app is ~150 lines of server code + ~120 lines HTML. It doesn't need a framework, database, or auth for personal use.


Would you use a tool like this? What features would you add?

Follow @armorbreak for more builder content.

Top comments (0)