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
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
Welcome in!”,“status”:“draft”}’
- curl -sX POST localhost:8000/versions -H “Content-Type: application/json” -d ‘{“name”:“Summer Greeting”,“content”:“Hi there
- Insert emoji at grapheme index 0 (prepend)
- curl -sX POST localhost:8000/versions/1/emojis/insert -H “Content-Type: application/json” -d ‘{“emoji”:“
”,“position”:0}’
- curl -sX POST localhost:8000/versions/1/emojis/insert -H “Content-Type: application/json” -d ‘{“emoji”:“
- 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.