Allow more advanced customization of the welcom message

Let users edit texts, add or move emojis , and save multiple versions to choose which one appears on opening. This would make the experience more personal and enjoyable.

User “< 1m read time”

Please let us know what product or what the heck you are talking about.

This is a developer community, and developers can offer whatever kind of interface they would like in their applications, including some kind of message that doesn’t actually come from the AI model, because AI requires some unique user input to take action on.


I let gpt-5-mini answer your question to see if it could run with whatever you might be talking about, where the AI being asked is a programming specialist, and that model, which cannot distinguish the developer message and its identity from the user task, went wild for 7,000 tokens of output.

Allow more customization (of something?) -> nonsense code

Advanced welcome message customization: editable text, movable emojis, multi-version selection

Make the welcome experience personal and fun with:

  • Rich text editing and emoji support (shortcodes like :sparkles: or native emoji)
  • Drag/move emojis within the text
  • Save multiple versions (draft/published), pick a default, or rotate/randomize on open
  • Simple, modern REST API you can wire into any UI

Below is a compact FastAPI backend that implements all of this, plus emoji utilities that treat complex emoji as single “grapheme” units.

Highlights:

  • Python 3.12, FastAPI, SQLModel (SQLite), emoji, regex
  • Endpoints to create/update versions, insert/move emojis, publish, set default, and serve the opening message
  • Robust emoji handling with grapheme-aware insert/move

Deliverable Artifacts (code)

# app.py
# Python 3.12+ — FastAPI backend for customizable welcome messages with emoji utilities

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Generator, Literal

import emoji
import regex as re
from fastapi import Depends, FastAPI, HTTPException, Query
from pydantic import BaseModel, Field, model_validator
from sqlmodel import Field as SQLField, Session, SQLModel, create_engine, select

# ------------------------------
# Config / DB
# ------------------------------

DB_URL = "sqlite:///./welcome.db"
engine = create_engine(DB_URL, echo=False, future=True)


def utcnow() -> datetime:
    return datetime.now(timezone.utc)


def get_session() -> Generator[Session, None, None]:
    with Session(engine) as session:
        yield session


# ------------------------------
# Data Models
# ------------------------------

Status = Literal["draft", "published"]


class WelcomeMessage(SQLModel, table=True):
    id: int | None = SQLField(default=None, primary_key=True)
    name: str = SQLField(index=True)
    content: str = SQLField(default="")
    status: Status = SQLField(default="draft", index=True)
    is_default: bool = SQLField(default=False, index=True)
    created_at: datetime = SQLField(default_factory=utcnow)
    updated_at: datetime = SQLField(default_factory=utcnow)


# ------------------------------
# Schemas (Pydantic)
# ------------------------------

def normalize_text(text: str) -> str:
    # Convert :shortcodes: to unicode emoji and normalize consistently
    # Using language="alias" preserves many popular shortcodes like :thumbsup:
    try:
        return emoji.emojize(text, language="alias")  # type: ignore[arg-type]
    except Exception:
        # Fallback: return unmodified if emojize fails
        return text


class VersionCreate(BaseModel):
    name: str = Field(min_length=1, max_length=120)
    content: str = ""
    status: Status = "draft"
    is_default: bool = False

    @model_validator(mode="after")
    def _normalize(self) -> "VersionCreate":
        self.content = normalize_text(self.content)
        return self


class VersionUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=120)
    content: str | None = None
    status: Status | None = None
    is_default: bool | None = None

    @model_validator(mode="after")
    def _normalize(self) -> "VersionUpdate":
        if self.content is not None:
            self.content = normalize_text(self.content)
        return self


class EmojiInsertRequest(BaseModel):
    emoji: str  # unicode emoji or :shortcode:
    position: int | None = Field(
        default=None,
        description="Grapheme index at which to insert the emoji. None appends to end.",
    )


class EmojiMoveRequest(BaseModel):
    from_index: int = Field(ge=0, description="Emoji index among emoji-only sequence")
    to_index: int = Field(ge=0, description="Emoji index among emoji-only sequence")


class VersionOut(BaseModel):
    id: int
    name: str
    content: str
    status: Status
    is_default: bool
    created_at: datetime
    updated_at: datetime


# ------------------------------
# Emoji / Grapheme utilities
# ------------------------------

GRAPHEME_RE = re.compile(r"\X", re.UNICODE)


def split_graphemes(s: str) -> list[str]:
    return GRAPHEME_RE.findall(s)


def is_emoji_cluster(g: str) -> bool:
    # Consider it an emoji if replacing emojis yields empty (i.e., pure emoji grapheme)
    # Works for single and multi-codepoint emoji clusters (ZWJ sequences, skin tones)
    return emoji.replace_emoji(g, replace="") == "" and g != ""


def insert_emoji(text: str, emj: str, pos: int | None) -> str:
    tokens = split_graphemes(text)
    char = normalize_text(emj)  # allow :shortcode:
    if pos is None:
        tokens.append(char)
    else:
        if pos < 0:
            pos = 0
        if pos > len(tokens):
            pos = len(tokens)
        tokens.insert(pos, char)
    return "".join(tokens)


def move_emoji(text: str, from_idx: int, to_idx: int) -> str:
    tokens = split_graphemes(text)
    emoji_positions = [i for i, g in enumerate(tokens) if is_emoji_cluster(g)]
    if not emoji_positions:
        raise ValueError("No emojis to move.")
    if from_idx < 0 or from_idx >= len(emoji_positions):
        raise IndexError("from_index out of bounds.")
    if to_idx < 0:
        to_idx = 0
    if to_idx > len(emoji_positions) - 1:
        to_idx = len(emoji_positions) - 1

    src_token_index = emoji_positions[from_idx]
    moving = tokens.pop(src_token_index)

    # Recompute emoji positions after removal
    emoji_positions_after = [i for i, g in enumerate(tokens) if is_emoji_cluster(g)]
    if not emoji_positions_after:
        # No emojis remain; place moving back at start (as the only emoji)
        insert_at = 0
    else:
        if to_idx < 0:
            insert_at = 0
        elif to_idx >= len(emoji_positions_after):
            insert_at = emoji_positions_after[-1] + 1
        else:
            insert_at = emoji_positions_after[to_idx]
    tokens.insert(insert_at, moving)
    return "".join(tokens)


# ------------------------------
# App and Routes
# ------------------------------

app = FastAPI(title="Welcome Message Customizer", version="1.0.0")


@app.on_event("startup")
def on_startup() -> None:
    SQLModel.metadata.create_all(engine)
    # Seed a version if DB is empty (optional)
    with Session(engine) as s:
        if not s.exec(select(WelcomeMessage).limit(1)).first():
            s.add(
                WelcomeMessage(
                    name="Default Welcome",
                    content=normalize_text("Welcome! :sparkles:"),
                    status="published",
                    is_default=True,
                )
            )
            s.commit()


# --- Helpers ---

def ensure_default_exclusive(session: Session, version: WelcomeMessage) -> None:
    # Only one published version can be default at a time.
    if version.status != "published":
        raise HTTPException(400, "Only published versions can be set as default.")
    session.exec(
        select(WelcomeMessage)
        .where(WelcomeMessage.is_default == True)  # noqa: E712
        .where(WelcomeMessage.id != version.id)
    )
    # Bulk unset others
    others = session.exec(
        select(WelcomeMessage)
        .where(WelcomeMessage.id != version.id)
        .where(WelcomeMessage.is_default == True)  # noqa: E712
    ).all()
    for o in others:
        o.is_default = False
        o.updated_at = utcnow()
        session.add(o)
    version.is_default = True
    version.updated_at = utcnow()
    session.add(version)


def get_version_or_404(session: Session, version_id: int) -> WelcomeMessage:
    version = session.get(WelcomeMessage, version_id)
    if not version:
        raise HTTPException(404, "Version not found.")
    return version


# --- CRUD Versions ---

@app.post("/versions", response_model=VersionOut, status_code=201)
def create_version(payload: VersionCreate, session: Session = Depends(get_session)) -> VersionOut:
    v = WelcomeMessage(
        name=payload.name,
        content=payload.content,
        status=payload.status,
        is_default=False,  # set below if requested
    )
    session.add(v)
    session.commit()
    session.refresh(v)

    if payload.is_default and v.status == "published":
        ensure_default_exclusive(session, v)
        session.commit()
        session.refresh(v)

    return VersionOut.model_validate(v.__dict__)


@app.get("/versions", response_model=list[VersionOut])
def list_versions(
    status: Status | None = Query(None),
    session: Session = Depends(get_session),
) -> list[VersionOut]:
    stmt = select(WelcomeMessage).order_by(WelcomeMessage.created_at.desc())
    if status:
        stmt = stmt.where(WelcomeMessage.status == status)
    rows = session.exec(stmt).all()
    return [VersionOut.model_validate(r.__dict__) for r in rows]


@app.get("/versions/{version_id}", response_model=VersionOut)
def get_version(version_id: int, session: Session = Depends(get_session)) -> VersionOut:
    v = get_version_or_404(session, version_id)
    return VersionOut.model_validate(v.__dict__)


@app.patch("/versions/{version_id}", response_model=VersionOut)
def update_version(
    version_id: int,
    payload: VersionUpdate,
    session: Session = Depends(get_session),
) -> VersionOut:
    v = get_version_or_404(session, version_id)

    if payload.name is not None:
        v.name = payload.name
    if payload.content is not None:
        v.content = payload.content
    if payload.status is not None:
        v.status = payload.status

    v.updated_at = utcnow()
    session.add(v)
    session.commit()
    session.refresh(v)

    # Handle default toggle if requested
    if payload.is_default is True:
        ensure_default_exclusive(session, v)
        session.commit()
        session.refresh(v)
    elif payload.is_default is False and v.is_default:
        v.is_default = False
        v.updated_at = utcnow()
        session.add(v)
        session.commit()
        session.refresh(v)

    return VersionOut.model_validate(v.__dict__)


@app.post("/versions/{version_id}/publish", response_model=VersionOut)
def publish_version(version_id: int, session: Session = Depends(get_session)) -> VersionOut:
    v = get_version_or_404(session, version_id)
    v.status = "published"
    v.updated_at = utcnow()
    session.add(v)
    session.commit()
    session.refresh(v)
    return VersionOut.model_validate(v.__dict__)


@app.post("/versions/{version_id}/default", response_model=VersionOut)
def set_default(version_id: int, session: Session = Depends(get_session)) -> VersionOut:
    v = get_version_or_404(session, version_id)
    ensure_default_exclusive(session, v)
    session.commit()
    session.refresh(v)
    return VersionOut.model_validate(v.__dict__)


@app.post("/versions/{version_id}/duplicate", response_model=VersionOut, status_code=201)
def duplicate_version(version_id: int, session: Session = Depends(get_session)) -> VersionOut:
    v = get_version_or_404(session, version_id)
    nv = WelcomeMessage(
        name=f"{v.name} (copy)",
        content=v.content,
        status="draft",
        is_default=False,
    )
    session.add(nv)
    session.commit()
    session.refresh(nv)
    return VersionOut.model_validate(nv.__dict__)


# --- Emoji operations ---

@app.post("/versions/{version_id}/emojis/insert", response_model=VersionOut)
def insert_emoji_endpoint(
    version_id: int,
    payload: EmojiInsertRequest,
    session: Session = Depends(get_session),
) -> VersionOut:
    v = get_version_or_404(session, version_id)
    v.content = insert_emoji(v.content, payload.emoji, payload.position)
    v.updated_at = utcnow()
    session.add(v)
    session.commit()
    session.refresh(v)
    return VersionOut.model_validate(v.__dict__)


@app.post("/versions/{version_id}/emojis/move", response_model=VersionOut)
def move_emoji_endpoint(
    version_id: int,
    payload: EmojiMoveRequest,
    session: Session = Depends(get_session),
) -> VersionOut:
    v = get_version_or_404(session, version_id)
    try:
        v.content = move_emoji(v.content, payload.from_index, payload.to_index)
    except (IndexError, ValueError) as e:
        raise HTTPException(400, str(e))
    v.updated_at = utcnow()
    session.add(v)
    session.commit()
    session.refresh(v)
    return VersionOut.model_validate(v.__dict__)


# --- Serving the opening message ---

@app.get("/welcome", response_model=VersionOut)
def get_opening_message(
    strategy: Literal["default", "random", "rotate"] = Query("default"),
    session: Session = Depends(get_session),
) -> VersionOut:
    q = select(WelcomeMessage).where(WelcomeMessage.status == "published")
    versions = session.exec(q).all()
    if not versions:
        raise HTTPException(404, "No published welcome messages found.")

    chosen: WelcomeMessage | None = None
    if strategy == "default":
        chosen = next((v for v in versions if v.is_default), None)
        if not chosen:
            # Fallback to newest published
            versions_sorted = sorted(versions, key=lambda v: v.created_at, reverse=True)
            chosen = versions_sorted[0]
    elif strategy == "random":
        import random

        chosen = random.choice(versions)
    elif strategy == "rotate":
        # Stateless rotation: select based on current minute among published
        versions_sorted = sorted(versions, key=lambda v: (v.created_at, v.id or 0))
        idx = int(datetime.now(timezone.utc).timestamp() // 60) % len(versions_sorted)
        chosen = versions_sorted[idx]

    return VersionOut.model_validate(chosen.__dict__)

How to run

  • Install dependencies:
    • pip install “fastapi>=0.112” “uvicorn[standard]>=0.30” “sqlmodel>=0.0.16” “emoji>=2.12” “regex>=2024.5.15”
  • Start the server:
    • uvicorn app:app --reload

Quick API walkthrough (curl)

  • Create a version
    • curl -sX POST localhost:8000/versions -H “Content-Type: application/json” -d ‘{“name”:“Summer Greeting”,“content”:“Hi there :sun_with_face: Welcome in!”,“status”:“draft”}’
  • Insert emoji at grapheme index 0 (prepend)
    • curl -sX POST localhost:8000/versions/1/emojis/insert -H “Content-Type: application/json” -d ‘{“emoji”:“:sparkles:”,“position”:0}’
  • Move the first emoji to be the last among emojis
    • curl -sX POST localhost:8000/versions/1/emojis/move -H “Content-Type: application/json” -d ‘{“from_index”:0,“to_index”:999}’
  • Publish and set as default
    • curl -sX POST localhost:8000/versions/1/publish
    • curl -sX POST localhost:8000/versions/1/default
  • Serve opening message
    • curl -s localhost:8000/welcome?strategy=default
    • curl -s localhost:8000/welcome?strategy=random
    • curl -s localhost:8000/welcome?strategy=rotate

Optional UI sketch: contenteditable + draggable emojis

  • Use a contenteditable div, wrap each emoji grapheme in a span[data-emoji], and enable drag-and-drop. Send resulting text to the API.
  • For emoji picking, EmojiMart, Twemoji, or native emoji picker can be used.

Example snippet (minimal demo idea):

<div id="editor" contenteditable="true" style="padding:12px;border:1px solid #ddd;border-radius:8px;">
  Welcome! <span draggable="true" data-emoji="✨">✨</span>
</div>

<script>
  const editor = document.getElementById('editor');

  editor.addEventListener('dragstart', (e) => {
    const t = e.target;
    if (t.matches('[data-emoji]')) {
      e.dataTransfer.setData('text/emoji', t.textContent);
      t.classList.add('dragging');
    }
  });
  editor.addEventListener('dragend', (e) => {
    const t = e.target;
    if (t.matches('[data-emoji]')) t.classList.remove('dragging');
  });
  editor.addEventListener('dragover', (e) => e.preventDefault());
  editor.addEventListener('drop', (e) => {
    e.preventDefault();
    const emoji = e.dataTransfer.getData('text/emoji');
    const range = document.caretRangeFromPoint(e.clientX, e.clientY);
    if (!range) return;
    // Insert at caret:
    const span = document.createElement('span');
    span.setAttribute('data-emoji', emoji);
    span.setAttribute('draggable', 'true');
    span.textContent = emoji;
    range.insertNode(span);
  });

  async function saveVersion(versionId) {
    // Extract plain text (including emoji) to send to /versions/{id}
    const content = editor.innerText; // or more sophisticated serialization
    await fetch(`/versions/${versionId}`, {
      method: 'PATCH',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({content})
    });
  }
</script>

Notes and extensions

  • Validation: reject empty names, limit content length
  • History/Audit: maintain edit history or add “notes” per version
  • AB testing: add rollout percentages or rules (e.g., by locale, device)
  • Scheduling: add start/end windows per version
  • Rich text: swap content to Markdown or JSON-based rich text schema if you need styles

If you want, I can tailor a small front-end editor that calls these endpoints and supports drag-and-drop emoji reordering out of the box.