Документы как компоненты.
DOCX на выходе.
Собирайте документы из небольших типизированных компонентов на том же JSX, который вы уже пишете. Используйте встроенные возможности ИИ для динамических документов и масштабируйте их генерацию с помощью нашего движка.
npx docxcelerate init my-documents {
"schemaVersion": "docxcelerate.config/v0",
"activePreset": "local",
"presets": {
"local": {
"build": { "outDir": "build" },
"upload": {
"endpoint": "",
"method": "POST",
"headers": {},
"body": "document"
}
},
"staging": {
"build": { "outDir": "build" },
"upload": {
"endpoint": "https://documents.staging.example.com/api/letters",
"method": "POST",
"headers": { "Authorization": "Bearer ${LETTERS_TOKEN}" },
"body": "document"
}
}
}
} import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* Formatting is ordinary TypeScript — no template language, so an Oxford comma
* and a singular/plural switch cost one line each.
*/
export const Conditions = paragraph<OfferData>({
id: "conditions",
render: (data) => {
const list = formatList(data.conditions);
const verb = data.conditions.length === 1 ? "condition is" : "conditions are";
return `This offer is conditional. The ${verb} ${list}. We will confirm ` +
`your place automatically once your results reach us.`;
},
});
function formatList(items: string[]): string {
if (items.length <= 1) return items[0] ?? "";
return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
} import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* An optional field changes the paragraph rather than leaving a gap — the kind
* of branch that is awkward in a mail-merge and trivial here.
*/
export const Fees = paragraph<OfferData>({
id: "fees",
render: (data) => {
const base = `You have been assessed as a ${data.feeStatus} fee payer, so ` +
`tuition for your first year will be ${data.tuitionFee}.`;
if (!data.scholarship) {
return `${base} Details of loans, bursaries and hardship funding are in ` +
`the enclosed funding guide.`;
}
return `${base} We are also pleased to award you the ` +
`${data.scholarship.name}, worth ${data.scholarship.amount} for the ` +
`duration of your course. No separate application is needed.`;
},
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const Greeting = paragraph<OfferData>({
id: "greeting",
render: (data) => `Dear ${data.applicantName},`,
}); export { Conditions } from "./conditions.node.ts";
export { Fees } from "./fees.node.ts";
export { Greeting } from "./greeting.node.ts";
export { NextSteps } from "./next-steps.node.ts";
export { Offer } from "./offer.node.ts";
export { SignOff } from "./signoff.node.ts";
export { TutorNote } from "./tutor-note.node.ts"; import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const NextSteps = paragraph<OfferData>({
id: "next-steps",
render: (data) =>
`To accept, reply through the applicant portal by ${data.replyBy}. ` +
`If you would like to visit ${data.college} before deciding, our open ` +
`afternoons run every Thursday through April and you are very welcome.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const Offer = paragraph<OfferData>({
id: "offer",
render: (data) =>
`I am delighted to offer you a place on the ${data.programme} at ` +
`${data.college}, beginning ${data.startDate}. Your application reference ` +
`is ${data.offerRef}; please quote it in any correspondence with us.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
export const SignOff = paragraph<OfferData>({
id: "sign-off",
render: (data) =>
`Yours sincerely, ${data.signatory.name}, ${data.signatory.title}, ${data.college}.`,
}); import { paragraph } from "docxcelerate";
import type { OfferData } from "../types.ts";
/**
* The one paragraph worth generating. Everything else in this document is
* deterministic, so this is the only node that needs an endpoint at all.
*/
export const TutorNote = paragraph<OfferData>({
id: "tutor-note",
placeholder: (data) =>
`A short personal note from ${data.interviewer} about ${data.applicantName}'s interview.`,
generalPrompt: (data) =>
`Write two warm, specific sentences from ${data.interviewer} to ` +
`${data.applicantName}, referring to their interest in ${data.portfolioTheme}. ` +
`Mention one thing they should read before term starts.`,
systemPrompt: () =>
"You are an admissions tutor. Be warm but never effusive, and never promise outcomes.",
negativePrompt: () =>
"Do not restate the offer, the conditions, or the reply deadline.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { OfferData } from "./types.ts";
export default defineDocumentProject<OfferData>({
id: "offer-of-admission",
name: "Offer of Admission",
version: "1.0.0",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import {
Conditions,
Fees,
Greeting,
NextSteps,
Offer,
SignOff,
TutorNote,
} from "./nodes/index.ts";
import type { OfferData } from "./types.ts";
export const documentTemplate = template<OfferData>(
<Document id="offer-of-admission" title="Offer of Admission">
<Section id="your-offer" title="Your offer">
<Greeting />
<Offer />
<TutorNote />
</Section>
<Section id="conditions" title="Conditions">
<Conditions />
</Section>
<Section id="fees-and-funding" title="Fees and funding">
<Fees />
</Section>
<Section id="next-steps" title="Next steps">
<NextSteps />
<SignOff />
</Section>
</Document>,
); import type { OfferData } from "./types.ts";
export const previewData: OfferData = {
applicantName: "Maya Oyelaran",
programme: "MEng Structural Engineering",
college: "Ashcroft College",
startDate: "28 September 2026",
offerRef: "ADM-2026-4417",
conditions: [
"grades A*AA at A-level, including Mathematics and Physics",
"IELTS 7.0 overall, with no component below 6.5",
"a satisfactory enhanced DBS check",
],
replyBy: "15 May 2026",
portfolioTheme: "timber gridshell roofs and low-carbon structural systems",
interviewer: "Dr Priya Raman",
tuitionFee: "£9,535",
feeStatus: "Home",
scholarship: { name: "Ashcroft Engineering Scholarship", amount: "£3,000 per year" },
signatory: { name: "Dr Priya Raman", title: "Director of Undergraduate Admissions" },
}; export interface OfferData {
applicantName: string;
programme: string;
college: string;
startDate: string;
offerRef: string;
conditions: string[];
replyBy: string;
/** Context for the tutor's note — never printed verbatim. */
portfolioTheme: string;
interviewer: string;
tuitionFee: string;
feeStatus: "Home" | "International";
scholarship?: { name: string; amount: string };
signatory: { name: string; title: string };
} import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
/**
* An absent optional field should change the sentence, not print "undefined"
* or leave a blank line.
*/
export const Access = paragraph<RepairsData>({
id: "access",
render: (data) =>
data.accessNotes
? `We hold the following access note for your home: ${data.accessNotes} ` +
`Please let us know if this is out of date.`
: `Someone aged 18 or over needs to be home for the visit. If that is ` +
`not possible, call us and we will arrange access another way.`,
}); import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const Appointment = paragraph<RepairsData>({
id: "appointment",
render: (data) =>
`We have booked a ${data.trade} to visit ${data.address} on ` +
`${data.visitDate}, ${data.visitWindow}. Your job reference is ` +
`${data.jobRef}.`,
}); import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const Greeting = paragraph<RepairsData>({
id: "greeting",
render: (data) => `Dear ${data.residentName},`,
}); export { Access } from "./access.node.ts";
export { Appointment } from "./appointment.node.ts";
export { Greeting } from "./greeting.node.ts";
export { WhatToExpect } from "./what-to-expect.node.ts"; import { paragraph } from "docxcelerate";
import type { RepairsData } from "../types.ts";
export const WhatToExpect = paragraph<RepairsData>({
id: "what-to-expect",
placeholder: (data) => `What to expect during a ${data.trade}'s visit.`,
generalPrompt: (data) =>
`In three short sentences, explain what a ${data.trade} will do during a ` +
`routine repair visit, how long it usually takes, and what the resident ` +
`should move out of the way beforehand.`,
systemPrompt: () =>
"You write for social housing residents. Use plain English and short sentences.",
negativePrompt: () => "Do not repeat the appointment date or the job reference.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { RepairsData } from "./types.ts";
export default defineDocumentProject<RepairsData>({
id: "repairs-appointment",
name: "Repair Appointment",
version: "2.1.0",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import { Access, Appointment, Greeting, WhatToExpect } from "./nodes/index.ts";
import type { RepairsData } from "./types.ts";
export const documentTemplate = template<RepairsData>(
<Document id="repairs-appointment" title="Your repair appointment">
<Section id="appointment" title="Your appointment">
<Greeting />
<Appointment />
</Section>
<Section id="on-the-day" title="On the day">
<WhatToExpect />
<Access />
</Section>
</Document>,
); import type { RepairsData } from "./types.ts";
export const previewData: RepairsData = {
residentName: "Tomasz Wójcik",
address: "12 Bracken Road, Leeds LS6 3QT",
jobRef: "RPR-88213",
trade: "plumber",
visitDate: "Tuesday 25 August",
visitWindow: "between 8am and 12pm",
accessNotes: "Key safe at the side gate; the code is with the scheme manager.",
}; export interface RepairsData {
residentName: string;
address: string;
jobRef: string;
trade: "plumber" | "electrician" | "joiner";
visitDate: string;
visitWindow: string;
accessNotes?: string;
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Greeting = paragraph<PolicyData>({
id: "greeting",
render: (data) => `Dear ${data.holderName},`,
}); export { Greeting } from "./greeting.node.ts";
export { PremiumChange } from "./premium-change.node.ts";
export { Renewal } from "./renewal.node.ts";
export { Shopping } from "./shopping.node.ts"; import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
/**
* A price rise is the sentence customers actually read. Computing the
* direction and the percentage locally means it can never contradict the
* figures printed beside it.
*/
export const PremiumChange = paragraph<PolicyData>({
id: "premium-change",
render: (data) => {
const delta = data.newPremium - data.lastPremium;
const percent = Math.abs((delta / data.lastPremium) * 100).toFixed(1);
if (Math.abs(delta) < 0.01) {
return `Your premium is unchanged at ${money(data.newPremium)} a year.`;
}
const direction = delta > 0 ? "risen" : "fallen";
return `Your premium has ${direction} by ${percent}%, from ` +
`${money(data.lastPremium)} to ${money(data.newPremium)} a year. ` +
`That works out at ${money(data.newPremium / 12)} a month.`;
},
});
function money(value: number): string {
return value.toLocaleString("en-GB", { style: "currency", currency: "GBP" });
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Renewal = paragraph<PolicyData>({
id: "renewal",
render: (data) =>
`Your ${data.cover.toLowerCase()} policy ${data.policyNumber} renews on ` +
`${data.renewalDate}. Unless you tell us otherwise, cover continues ` +
`automatically with an excess of ${money(data.excess)}.`,
});
function money(value: number): string {
return value.toLocaleString("en-GB", { style: "currency", currency: "GBP" });
} import { paragraph } from "docxcelerate";
import type { PolicyData } from "../types.ts";
export const Shopping = paragraph<PolicyData>({
id: "shopping-around",
placeholder: () => "A short note on comparing this renewal with other quotes.",
generalPrompt: (data) =>
`Write two sentences reminding ${data.holderName} that they can compare ` +
`this renewal against other quotes, and that doing so will not affect ` +
`their existing cover.`,
systemPrompt: () =>
"You write regulated insurance correspondence. Be neutral and never discourage switching.",
negativePrompt: () => "Do not quote a price, a percentage, or a competitor name.",
}); import { cleanMinimalDocumentStyle } from "docxcelerate";
import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { previewData } from "./preview-data.ts";
import type { PolicyData } from "./types.ts";
export default defineDocumentProject<PolicyData>({
id: "policy-renewal",
name: "Policy Renewal",
version: "0.4.2",
template: documentTemplate,
style: cleanMinimalDocumentStyle,
previewData,
}); import { Document, Section, template } from "docxcelerate/template";
import { Greeting, PremiumChange, Renewal, Shopping } from "./nodes/index.ts";
import type { PolicyData } from "./types.ts";
export const documentTemplate = template<PolicyData>(
<Document id="policy-renewal" title="Your renewal">
<Section id="renewal" title="Your renewal">
<Greeting />
<Renewal />
<PremiumChange />
</Section>
<Section id="your-options" title="Your options">
<Shopping />
</Section>
</Document>,
); import type { PolicyData } from "./types.ts";
export const previewData: PolicyData = {
holderName: "Eleanor Whitfield",
policyNumber: "HC-2291-8840",
cover: "Contents and buildings",
renewalDate: "1 October 2026",
lastPremium: 284.4,
newPremium: 311.16,
excess: 250,
}; export interface PolicyData {
holderName: string;
policyNumber: string;
cover: "Contents" | "Buildings" | "Contents and buildings";
renewalDate: string;
lastPremium: number;
newPremium: number;
excess: number;
} Настоящий предпросмотр документа в момент отрисовки — без скриншотов и скрытых уловок.
Пишите документы как сайты
Документ — это дерево типизированных компонентов. Если вы писали на React, эта форма вам уже знакома: пропсы, композиция, небольшие файлы. Поэтому фронтенд-разработчик становится продуктивным в первый же день, а не после изучения очередного языка шаблонов.
Спроектировано под ИИ
Сгенерированный текст — это тип узла, а не надстройка. Отметьте тот единственный абзац, которому действительно нужна модель, задайте ему промпты и заполнитель, а остальное оставьте детерминированным — так ИИ решает сложную часть, не отдавая ему на откуп весь документ.
Документы живут в вашем репозитории
Раз документ — это исходный код, изменение одной фразы становится pull request: с диффом, с ревью и с авторством, которое можно установить и год спустя, когда кто-нибудь спросит, кто поправил формулировку о задолженности. Тесты фиксируют, что документ рендерится так, как вы ожидаете, и CI замечает ошибку раньше получателя.
Docxcelerate обращается с документом так же, как UI-фреймворк обращается с экраном: небольшие компоненты, собранные в дерево и отрисованные тем, что разбирается в бумаге. Вы получаете удобство компонентной модели, а получатель получает документ Word.
Движок
Именно в движке документы и пишутся по-настоящему. Он подставляет ваши данные, запускает ИИ и возвращает готовый документ. Использовать ИИ может узел любого вида, не только абзац. Ответ модели либо становится текстом, написанным по тем сведениям, которые вы ей дали, либо принимает решение, от которого зависит документ.
Шаблон публикуется в движок один раз. После этого любая система может вызвать его API с набором данных и получить документ. Бесплатный движок можно разместить у себя. Управляемое облако запускает полную версию — с многим, чего в бесплатной нет, — и появится скоро.
- 01 Сборка Фреймворк превращает ваш документ в пакет. Этот шаг выполняется на вашей машине.
- 02 Публикация Вы отправляете пакет в движок. Движок сохраняет его и даёт ему имя.
- 03 Написание Ваше приложение отправляет набор данных. Движок возвращает готовый документ.
Прочитайте целиком
Каждый тип узла, каждый флаг CLI и каждый файл, который пишет сборка — с предпросмотрами, отрисованными настоящим рендерером, так что ничто на странице не может описывать хелпер, которого больше нет.