How the Prompt Template (Mask) System Works in NextChat: A Technical Deep Dive
NextChat's mask system lets users pre-define conversation contexts, model configurations, and languages that automatically initialize new chat sessions, blending built-in templates with user-created presets stored in a persistent Zustand store.
NextChat (formerly ChatGPTNextWeb) implements a sophisticated prompt template architecture called "masks" that streamlines how users initiate AI conversations. The mask system combines static built-in templates with dynamic user-defined configurations, enabling instant application of complex system prompts and model settings. According to the NextChat source code, this system is implemented through a layered architecture involving TypeScript type definitions, persistent state management, and build-time optimization.
The Mask Data Structure
At the core of the system is the Mask type defined in app/store/mask.ts (lines 9–23). This interface defines the complete shape of a prompt template:
export type Mask = {
id: string; // unique identifier (nanoid)
createdAt: number; // timestamp
avatar: string; // UI icon
name: string; // display name
hideContext?: boolean; // hide context from UI
context: ChatMessage[]; // preset messages (system / user / assistant)
syncGlobalConfig?: boolean; // if true, model config follows global config
modelConfig: ModelConfig; // model, temperature, token limits, etc.
lang: Lang; // language of the mask
builtin: boolean; // true for shipped masks
plugin?: string[]; // optional plugin ids
enableArtifacts?: boolean; // UI flags
enableCodeFold?: boolean;
};
The context array stores the actual prompt template—an array of ChatMessage objects that typically include a system message defining the AI's persona plus optional example exchanges. The modelConfig property overrides global settings for that specific mask, controlling parameters like temperature, max tokens, and model selection.
Built-in Masks: Static Template Definitions
NextChat ships with pre-configured prompt templates stored as static JavaScript objects in locale-specific files. The built-in masks reside in app/masks/en.ts, app/masks/cn.ts, and app/masks/tw.ts, each exporting an array of BuiltinMask objects tailored to English, Simplified Chinese, and Traditional Chinese users respectively.
Each built-in mask contains:
- Avatar and name – Visual identifiers for the UI
- Context array – Pre-loaded conversation messages (typically system prompts)
- Model configuration – Default model, temperature, and token limits
- Language tag – Locale association for organization
- Builtin flag – Set to
trueto distinguish from user masks
For example, the GitHub Copilot mask in app/masks/en.ts (lines 4–28) defines a programming assistant persona:
{
avatar: "1f47e",
name: "GitHub Copilot",
context: [{
id: "Copilot-0",
role: "system",
content: "You are an AI programming assistant..."
}],
modelConfig: {
model: "gpt-4",
temperature: 0.3,
max_tokens: 2000
},
lang: "en",
builtin: true,
createdAt: 1688899480410,
}
The Mask Store: Persistence and Runtime Logic
User-created masks and runtime operations are managed by useMaskStore in app/store/mask.ts. This Zustand-based store persists data to localStorage or IndexedDB while handling the complex logic of merging user masks with built-in templates.
Creating New Masks
When users click "New Mask," the store executes the create() method (lines 35–47). This function generates a unique ID using nanoid(), merges user-provided overrides with defaults from createEmptyMask(), and marks the result as non-built-in:
const id = nanoid();
masks[id] = {
...createEmptyMask(),
...mask, // user-provided overrides
id,
builtin: false,
};
The createEmptyMask() helper derives initial values from the current global application configuration (useAppConfig.getState().modelConfig) and the detected interface language (getLang()), ensuring new masks inherit sensible defaults while remaining customizable.
Retrieving and Merging Masks
The getAll() method (lines 88–105) handles the sophisticated logic of presenting masks to the UI. It returns user masks first, sorted by creation date (newest to oldest), then conditionally appends built-in masks unless the hideBuiltinMasks global setting is enabled:
const userMasks = Object.values(get().masks).sort(
(a, b) => b.createdAt - a.createdAt,
);
if (!config.hideBuiltinMasks) {
const buildinMasks = BUILTIN_MASKS.map(m => ({
...m,
modelConfig: { ...config.modelConfig, ...m.modelConfig },
}));
return userMasks.concat(buildinMasks);
}
return userMasks;
Notice the modelConfig merge strategy: built-in masks inherit the user's global configuration, then apply their own specific overrides. This allows template-specific temperature or token limits while respecting user preferences for model selection.
Build-Time Optimization: Generating masks.json
To optimize frontend performance, NextChat separates built-in mask definitions from the main bundle. During the production build process, app/masks/build.ts aggregates all locale-specific mask arrays into a single JSON file at public/masks.json:
fs.writeFile(
dirname + "/../../public/masks.json",
JSON.stringify(BUILTIN_MASKS, null, 4),
...
);
The frontend fetches this JSON file to populate the mask selector without importing the full TypeScript source, reducing bundle size and enabling dynamic updates to built-in templates without recompiling the application.
Runtime Application: From Mask to Conversation
When initiating a chat session with a selected mask, the application performs two critical operations:
- Context Injection – The
contextarray from the mask is pre-loaded into the chat message list, establishing the system prompt and any example conversations immediately. - Configuration Override – The mask's
modelConfigbecomes the active configuration for API requests, controlling generation parameters.
If the mask's syncGlobalConfig property is set to true, the system maintains a live connection to global settings. Changes to the application's model configuration automatically propagate to the mask, ensuring consistency without manual updates.
Code Examples
Creating a Custom Mask Programmatically
import { useMaskStore } from '@/app/store/mask';
const myMask = useMaskStore.getState().create({
name: 'My Research Assistant',
avatar: '🧠',
context: [
{
id: 'research-0',
role: 'system',
content: 'You are a concise research assistant...'
},
],
modelConfig: {
model: 'gpt-3.5-turbo',
temperature: 0.2,
max_tokens: 1500
},
});
Loading a Mask into a New Chat
import { useMaskStore } from '@/app/store/mask';
import { useChatStore } from '@/app/store/chat';
function startChatWithMask(maskId: string) {
const mask = useMaskStore.getState().get(maskId);
const chat = useChatStore.getState();
// preload the mask's context
chat.setMessages([...mask.context]);
// apply the mask's model config
chat.updateConfig(mask.modelConfig);
}
Fetching Built-in Masks on the Client
// masks are served as public/masks.json
fetch('/masks.json')
.then(r => r.json())
.then((builtinMasks) => {
// builtinMasks is { cn: [...], en: [...], tw: [...] }
console.log('Available built‑in masks:', builtinMasks);
});
Summary
- Masks are comprehensive prompt templates defined by the
Masktype inapp/store/mask.ts, combining conversation context with model configuration parameters. - Built-in masks ship as static objects in locale files (
app/masks/en.ts, etc.) and get compiled topublic/masks.jsonduring build for efficient loading. - User masks persist in a Zustand store that handles creation timestamps, unique ID generation via
nanoid(), and merging with global configuration settings. - The
getAll()method prioritizes user masks sorted by recency, then conditionally merges built-in masks with current global settings unless hidden. - Runtime application injects context messages and overrides model parameters when starting a chat, with optional synchronization to global config changes via
syncGlobalConfig.
Frequently Asked Questions
What is the difference between a built-in mask and a user-created mask?
Built-in masks are static JavaScript objects shipped with the application in files like app/masks/en.ts, marked with builtin: true, and compiled into public/masks.json. User-created masks are generated dynamically through the create() method in app/store/mask.ts, assigned unique IDs via nanoid(), marked with builtin: false, and stored persistently in the browser's localStorage or IndexedDB through the Zustand store.
How does NextChat handle mask configuration when global settings change?
When a mask's syncGlobalConfig property is true, it dynamically inherits changes from the global application configuration. In the getAll() method, built-in masks explicitly merge the current global modelConfig with their own settings using the spread operator: { ...config.modelConfig, ...m.modelConfig }. This ensures user preferences for model selection apply to all masks while preserving template-specific parameters like temperature.
Where are custom masks stored in the browser?
Custom masks persist through the createPersistStore helper (defined in app/utils/store.ts), which wraps the Zustand store with localStorage or IndexedDB backing. The useMaskStore in app/store/mask.ts maintains the masks object—a dictionary keyed by mask ID—saving both user-created masks and their creation timestamps across browser sessions.
Can masks include plugin configurations?
Yes. The Mask type includes an optional plugin property defined as string[], allowing masks to specify which plugins should be active for conversations using that template. Additionally, UI-specific flags like enableArtifacts and enableCodeFold control interface behaviors independent of the AI model configuration, letting masks customize both backend processing and frontend presentation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →