Skip to main content
The AI assistant knows about Roger because every request to POST /api/chat includes a system prompt built from the live portfolio data. That prompt is generated by generatePortfolioContext() in data/portfolio-context.ts.

How It Works

generatePortfolioContext() is a plain TypeScript function that:
  1. Imports the four portfolio data arrays (skills, experiences, educations, projects, socialLinks).
  2. Formats each array into a human-readable text block.
  3. Returns a single string that is passed directly to streamText as the system parameter.
// data/portfolio-context.ts
import { educations, experiences } from "@/data/experience";
import { projects } from "@/data/projects";
import { skills } from "@/data/skills";
import { socialLinks } from "@/data/socials";

export function generatePortfolioContext(): string {
  // ... format each data source ...
  return `...system prompt string...`;
}

Function Signature

export function generatePortfolioContext(): string
The function takes no arguments and returns a string. It is called by the route’s getPortfolioContext() helper, which caches the result for 5 minutes.

Data Sources

The function pulls from five data files under data/:
ImportFileWhat it contributes to the prompt
skillsdata/skills.tsSkill categories and the technologies in each (skill.title, skill.techs)
experiencesdata/experience.tsWork experience entries (title, sub_title, years, details)
educationsdata/experience.tsEducation entries (same shape as experience)
projectsdata/projects.tsProject name, description, tech tags, and demo URL
socialLinksdata/socials.tsSocial/contact links (label, href)

How Each Source Is Formatted

// Skills: "- Frontend: React, Next.js, TypeScript"
const skillsText = skills
  .map((skill) => `- ${skill.title}: ${skill.techs.join(", ")}`)
  .join("\n");

// Projects: "- Project Name: description (Technologies: ...). Demo: url"
const projectsText = projects
  .map(
    (p) =>
      `- ${p.title}: ${p.description} (Tecnologías: ${p.tags.join(", ")}). Demo: ${p.demo}`,
  )
  .join("\n");

// Experience: "- Role at Company (year range): details"
const experienceText = experiences
  .map(
    (exp) =>
      `- ${exp.title} en ${exp.sub_title} (${exp.years}): ${exp.details}`,
  )
  .join("\n");

// Education (same shape as experience)
const educationText = educations
  .map(
    (edu) =>
      `- ${edu.title} en ${edu.sub_title} (${edu.years}): ${edu.details}`,
  )
  .join("\n");

// Socials: "- GitHub: https://github.com/..."
const socialsText = socialLinks
  .map((s) => `- ${s.label}: ${s.href}`)
  .join("\n");

System Prompt Structure

The returned string is divided into four sections:
  1. Persona — The assistant is introduced as Roger’s personal assistant. The tone is described: professional, natural, with occasional emojis.
  2. About Roger — Static biographical information (name, location, stack, values, personal interests) that is hardcoded directly in the template literal.
  3. Dynamic data — The five formatted text blocks from the data files are interpolated here: skills, experience, education, projects, and social links.
  4. Assistant role instructions — Rules for how to respond: response length (150–200 words), emoji usage (max 3), formatting, handling unknown information, and what not to invent.

Keeping the Context Up to Date

Because generatePortfolioContext() reads directly from the same data files that drive the rest of the portfolio, updating the AI’s knowledge is the same as updating the portfolio itself:
  • New project → add an entry to data/projects.ts. The AI will mention it on the next context refresh (within 5 minutes).
  • New job → add an entry to the experiences array in data/experience.ts.
  • New skill → add an entry to data/skills.ts.
  • New social link → add an entry to data/socials.ts.
During development you can set CONTEXT_CACHE_DURATION to 0 in app/api/chat/route.ts so context changes take effect immediately without waiting for the cache to expire.

Customisation Guide

Updating the AI’s Personality and Tone

The personality is defined in two places inside the template literal in portfolio-context.ts: Opening description (controls the overall persona):
return `
 Eres el asistente personal de Roger Civ, desarrollador web full stack.
Hablas sobre él con cercanía, criterio técnico y personalidad.
Eres profesional, natural y ocasionalmente usas emojis con moderación.
// ...
`;
Role instructions (controls response style and limits):
📋 TU ROL COMO ASISTENTE

Cómo debes responder:
- Habla como un colega técnico que conoce bien a Roger
- claro, directo y útil
- Usa humor ligero cuando encaje
// ...

Estilo:
- Máx. 150200 palabras
- Formato flexible: lista o narrativa según convenga
- 13 emojis como máximo
- Tono humano, sin marketing vacío
Edit these sections directly to adjust language, tone, length limits, or emoji usage.

Adding Static Information That Doesn’t Fit in Data Files

For facts that aren’t naturally structured as a list — a personal statement, an explanation of a career change, a preferred contact method — add them directly to the template literal in the SOBRE ROGER section:
return `
// ...
👤 SOBRE ROGER

Nombre: Roger Civ
// ... existing fields ...

// Add new static content here, for example:
Sobre disponibilidad:
- Abierto a proyectos freelance remotos a partir de septiembre 2025
- Disponible para entrevistas con una semana de antelación

═══════════════════════════════════════════════════════════════
// ...
`;

Adding Custom Q&A Pairs

You can guide the AI to answer specific questions in a specific way by appending an FAQ block to the system prompt:
return `
  // ... existing prompt ...

❓ PREGUNTAS FRECUENTES

P: ¿Está Roger disponible para trabajo remoto?
R: Sí, Roger trabaja principalmente en remoto y está abierto a colaboraciones internacionales.

P: ¿Cuánto cobra Roger por un proyecto freelance?
R: Los precios varían según el alcance. Lo mejor es contactarle directamente en [email protected].
`;
Keep Q&A pairs short and specific. Long answers in the system prompt can crowd out space for the conversation history and reduce response quality.

Best Practices for Writing Effective Context

  • Be concrete. The model responds better to specific facts (“5 years of React experience”) than vague claims (“extensive experience”).
  • Use consistent formatting. The current prompt uses - label: value lists. Stick to this pattern so the model can reliably parse the data.
  • Set clear limits. The “Límites” section explicitly tells the model not to invent projects or share unlisted personal information. Always include a similar guardrail section.
  • Keep it focused. Every sentence in the system prompt consumes tokens on every request. Remove sections that are not useful.
  • Test iteratively. After any change, ask the chatbot questions that probe the updated section to verify it responds as expected.
  • Separate persona from data. Keep the personality instructions and the data blocks distinct so you can update one without accidentally breaking the other.

Build docs developers (and LLMs) love