Skip to content

Conversation

@SantiagoSeisdedos
Copy link

@SantiagoSeisdedos SantiagoSeisdedos commented Sep 29, 2025

Description

This PR delivers my implementation for the Fullstack Engineer Challenge – AI Content Workflow.

Summary of Changes

  • Backend (NestJS + Prisma + PostgreSQL)

    • Implemented modular NestJS backend with Prisma ORM
    • Added Campaign and Content Piece CRUD
    • AI Draft generation with multi-model support (OpenAI + Anthropic)
    • WebSocket gateway with real-time updates for campaigns, content pieces, drafts, and translations
    • Cost tracking: per draft, per content piece, and per campaign
    • Document upload (PDF, TXT) with RAG (retrieval-augmented generation)
    • Web search integration to enrich AI context
  • Frontend (Next.js + React Query + Tailwind + shadcn/ui)

    • Dashboard for campaigns and content pieces
    • Editor for reviewing/editing drafts/translations
    • Real-time updates using Socket.IO client
    • AI model selector (6 available models with cost preview 3 openai / 3 cloude)
    • Cost analytics displayed at draft, content piece, and campaign levels
    • Document upload UI with retrieval context for AI generation
  • Infrastructure

    • Docker Compose setup with PostgreSQL, Redis, backend, and frontend
    • .env.example with required API keys and connection strings
    • Health checks, cURL examples, and usage guide in SETUP_AND_USAGE.md

Fixes # (no issue was opened – challenge submission).


Type of Change

  • New feature (non-breaking change which adds functionality)
  • Documentation update

How Has This Been Tested?

  • Verified backend APIs locally using cURL and Postman:
    • Campaign and content piece creation
    • AI draft generation with different models
    • Review state transitions (Draft → Suggested → Reviewed → Approved/Rejected)
    • Real-time updates across multiple clients (tested with normal + incognito sessions)
  • Verified frontend by running locally (npm run dev):
    • Campaign dashboard functionality
    • Real-time sync between two browser sessions
    • Document upload and RAG functionality
  • Verified cost tracking updates correctly at draft, content piece, and campaign levels

Demo Video

Quick Start (recommended)

1. Clone fork and checkout branch

git clone <your-fork-url>
cd fullstack-engineer-ai-content-workflow-challenge
git fetch --all
git checkout SantiagoSeisdedosImplementation

2. Environment setup

cp .env.example .env

  • Required: OPENAI_API_KEY or ANTHROPIC_API_KEY
  • Optional: SERPER_API_KEY (for Google search enrichment)

Note: Get your key here: (serper free key)

3. Run all services

docker-compose up -d
docker-compose ps # check container status

Important: Make sure you have Docker Desktop running!

4. Apply Prisma migrations inside backend container

docker exec -it acme-backend npx prisma migrate deploy

5. Access frontend & backend

🔗frontend
🔗backend

Dev Mode (optional, if you want to run frontend/backend locally)

Run infra only (Postgres + Redis)

docker-compose up -d postgres redis

Backend (terminal 1)

cd backend
npm install
npx prisma migrate dev --name init
npm run start:dev

Frontend (terminal 2)

cd frontend
npm install
npm run dev

@joaquinzapata1
Copy link

Hola Santi! Soy Joaco de NaNLABS. Estoy a cargo de revisar tu challenge. 🥷

Muy bueno lo que vi hasta ahora, y muchas gracias por la demo!
Se nota que te gusta mucho lo que haces y que estás atento a los detalles. 🔥

Me queda pendiente para el lunes revisar más en detalle para poder dejarte algo de feedback más concreto.

Que tengas un gran finde!

Copy link

@joaquinzapata1 joaquinzapata1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hola de nuevo Santi! Te dejo algunos comentarios. La verdad que en general lo vi muy prolijo y me gustaron varias de las ideas que tuviste e implementaste. Por ejemplo el Chain of Thought es genial de ver y de usar, y lo de los costos puede ser muy útil más allá de que tiene algún bug dando vueltas.

Pude levantarlo localmente, ahí usé los pasos de la documentación SETUP_AND_USAGE.md y tuve algunos problemas menores. En instructions.md hay una lista similar de pasos para levantar los servicios, y en la descripción del PR hay otra más (que parece la correcta).

Me funcionó bien, probé varios casos y tanto el backend como la UI son bastante responsivos e intuitivos. Ahí te dejé una sugerencia también para el hook del websocket que parece que está un poco bugueado.

Fuiste con todo y el resultado quedó muy bien. Muchos de los "nice to have" que se mencionan en la consigna también los incluiste y eso suma puntos.


Como curiosidad, te consulto:

  • Usaste alguna herramienta de GenAI? Cuáles y por qué?
  • Te quedaste con ganas de mejorar alguna parte del sistema? Cuál/es?

Desde ya muchas gracias por el tiempo que le dedicaste! 🧨

import { useState, useEffect, useCallback } from "react";
import { socketService } from "@/lib/websocket/socket";

interface ChainOfThoughtsState {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Está buenísimo este componente. Además de que se ve muy bien, es clave para el usuario saber qué está haciendo el LLM específicamente. Y sumándole la implementación de websockets se lo podemos mostrar en tiempo real.

Es toltamente overkill para lo que es el challenge pero está genial que tires magias 🪄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me alegra que te haya gustado! Habia implementado algo parecido antes en sphereone, la diferencia es que por motivos de arquitectura/limitaciones tuvimos que mudar de sockets directos a messages guardados en la db con listeners en el front para "emitir" los onCreate. Mira aca te paso una demo rapida

screen-capture.webm

COPY package*.json ./

# Install dependencies
RUN npm ci --only=production --legacy-peer-deps

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Al intentar levantar locamente, me falló el compose en esta parte.

El --only=production provoca que no se instale @nestjs/cli (dev), entonces no existe nest para compilar. Solución: (1) quitar la flag en dev (lo que hice yo, suficiente para ambientes no productivos), o (2) usar multi-stage: compilar con dev deps y luego prunar a prod para el runtime.

Ejemplo de la opción 2 (AI generated así que puede fallar):

# deps (instala dev+prod)
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --legacy-peer-deps

# build (compila)
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

# runtime (solo prod)
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
COPY --from=builder /app/node_modules ./node_modules
RUN npm prune --production

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma

EXPOSE 3001
CMD ["node", "dist/main.js"]

cd fullstack-engineer-ai-content-workflow-challenge

# Copy environment variables
cp .env.example .env

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acá agregaría que también se necesita copiar el .env del directorio /backend, para que prisma tenga de donde sacar el DATABASE_URL

cd backend

# Run database migrations
npx prisma migrate dev --name init

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Siguiendo con el comentario de arriba, si no copiaramos el .env podríamos aprovechar las variables de entorno que ya tenemos en el container y correr el comando dentro del mismo, como mencionás en la descripción del PR

-d '{
"name": "Q4 Product Launch",
"description": "Marketing campaign for our new product launch",
"targetAudience": "Tech professionals",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tanto targetAudience como budget quedaron viejos de algún draft. Los demás curl me respondieron perfecto

connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'error';
}

export const useWebSocket = () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este debe tener un bug en algún lado, por lo que vimos en la demo de los múltiples toast. Localmente me pasó lo mismo y en red me figura que tengo 3 websocket con 3 client id diferentes. Algunas de las cosas que se pueden revisar:

  • Hacer el efecto sin dependencias ([]) para no recrear la conexión por cambios de addToast.
  • Desconectar el socket en el cleanup del mismo efecto.
  • (Opcional) Hacer que socketService.connect() sea idempotente (si ya hay socket, reutilizarlo).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si, totalmente. Si te das cuenta, en el video cuando vi el doble toast (que hasta ese momento pensaba que lo habia resuelto) me quede recalculando unos segundos jajaja. No lo volvi a revisar por falta de tiempo. Fue una de las cosas que me quedo pendiente.

@@ -0,0 +1,191 @@
// Centralized error handling and API response management

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Siempre falta algo como esto en los proyectos y nos damos cuenta demasiado tarde. Una solución sencilla para error handling 🔥

@@ -0,0 +1,557 @@
"use client";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este file es gigante. Un enfoque que se puede tomar acá es buscar alguna librería de manejo de estados que nos aliviane un poco la carga.

Ejemplo con zustand:

// store/realtime.ts
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { socketService } from "@/lib/websocket/socket";

type State = {
  campaigns: Campaign[];
  contentPieces: Record<string, ContentPiece[]>;
  drafts: Record<string, Draft[]>;
  documents: Record<string, Document[]>;
  hydrate: () => void; // registra listeners 1 sola vez
};

export const useRealtime = create<State>()(immer((set, get) => ({
  campaigns: [],
  contentPieces: {},
  drafts: {},
  documents: {},
  hydrate: () => {
    // idempotente
    socketService.connect();
    socketService.onCampaignCreated((c) => {
      set(s => { s.campaigns.push(c as any); });
    });
    // ...el resto de eventos con set(s => {...}) y listo
  },
})));

Copy link
Author

@SantiagoSeisdedos SantiagoSeisdedos Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si, la verdad es que ahora lo vuelvo a mirar y es horrible este archivo. Nunca use zustand, le voy a dar una oportunidad la proxima!

};

// Get user-friendly error message from status code
export const getErrorMessage = (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tanto getErrorMessage como handleApiResponse son usados solo en este file así que no debería ser necesario exportarlos

@SantiagoSeisdedos
Copy link
Author

Hola de nuevo Santi! Te dejo algunos comentarios. La verdad que en general lo vi muy prolijo y me gustaron varias de las ideas que tuviste e implementaste. Por ejemplo el Chain of Thought es genial de ver y de usar, y lo de los costos puede ser muy útil más allá de que tiene algún bug dando vueltas.

Pude levantarlo localmente, ahí usé los pasos de la documentación SETUP_AND_USAGE.md y tuve algunos problemas menores. En instructions.md hay una lista similar de pasos para levantar los servicios, y en la descripción del PR hay otra más (que parece la correcta).

Me funcionó bien, probé varios casos y tanto el backend como la UI son bastante responsivos e intuitivos. Ahí te dejé una sugerencia también para el hook del websocket que parece que está un poco bugueado.

Fuiste con todo y el resultado quedó muy bien. Muchos de los "nice to have" que se mencionan en la consigna también los incluiste y eso suma puntos.

Como curiosidad, te consulto:

  • Usaste alguna herramienta de GenAI? Cuáles y por qué?
  • Te quedaste con ganas de mejorar alguna parte del sistema? Cuál/es?

Desde ya muchas gracias por el tiempo que le dedicaste! 🧨

Muchas gracias por el feedback tan detallado y positivo! Me alegra que hayas podido levantarlo localmente y que te haya funcionado bien en los tests. Tomo nota de lo de las instrucciones duplicadas en los MDs y la descripción del PR. También chequeo el hook del websocket, gracias por la sugerencia.

Sobre la curiosidad: sí, usé algunas herramientas de GenAI para agilizar el proceso, pero siempre con un rol de asistente y no como reemplazo. Empecé charlando con ChatGPT para desglosar el README inicial, entender bien los requisitos y brainstormear ideas extras (como el Chain of Thought y los costos). De ahí, armamos un "roadmap" básico para tener contexto claro. Luego, pasé a Cursor como herramienta principal: le creé tres archivos guía (un "prompt" inicial con el overview, "project rules" basado en el README y nuestras decisiones, y "project status" como un historial dinámico de avances, pendientes y blockers). Los subí al folder (pero sin agregar a git) para que Cursor tuviera todo el contexto y me asistiera en el coding. Funcionó genial para velocidad – por ejemplo, generaba snippets rápidos que revisaba y ajustaba manualmente cuando no encajaban perfecto (como en algunos casos de lógica compleja).

Por qué? Principalmente por eficiencia en un tiempo limitado: permite entregar rápido y bien, dándole contexto sólido para que no "alucine" y yo pueda enfocarme en lo custom. A la par, aproveché para refrescar conceptos que tenía oxidados (como NestJS con Prisma, que repasé con videos antes de arrancar).

En cuanto a mejoras que me quedaron pendientes, varias! Por ejemplo, la generación de imágenes basada en la campaña o el draft – me hubiera encantado integrar algo como NanoBanana o DALL-E para visuals rápidos del producto/local, y si daba, hasta Sora para spots cortos de video (suponiendo un escenario de marketing de producto). Otra es un "modo respuesta doble": generar dos drafts distintos de texto al momento, como el estilo espejo de ChatGPT o Grok, para dar opciones variadas al usuario.
Y en el model selector/pricing, quería medir el consumo real por llamada (con Autogen se puede trackear fácil), en vez de la tabla mockeada del dashboard de OpenAI – quedó plano, pero con más tiempo lo habría dinamizado.

De nuevo, gracias por el desafío y el feedback – fue un placer dedicarle el tiempo.

Un abrazo y éxito! 🧨
Santi (mejorado con Grok)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants