# Как писать узлы

> Узел — это небольшой компонент, который принимает свои данные и возвращает то, что хочет сказать.

Source: https://docxcelerate.com/ru/docs/writing-nodes/

Документ — это дерево **компонентов**. Каждый возвращает **узел** — абзац,
изображение, график — и каждый живёт в собственном файле в `nodes/`. Документы
становятся длинными, и шаблон, вставляющий текст прямо в себя, перестаёт быть
читаемым примерно на половине пути.

## Сгенерируйте один

```sh
dxcl document node documents/welcome next-steps --type paragraph
```

Это запишет `nodes/next-steps.node.tsx` и добавит его в `nodes/index.ts`.
`--type` принимает `paragraph`, `image` или `graph`; запустите команду без
аргументов, чтобы вас спросили.

Размещение намеренно оставлено вам. Откройте `document.tsx` и добавьте компонент
в то место документа, которому он принадлежит:

```tsx
import { Document, Section, template } from "docxcelerate/template";
import * as Nodes from "./nodes/index.ts";
import type { DocumentData } from "./types.ts";

export const documentTemplate = template<DocumentData>(
  <Document id="welcome" title="Welcome">
    <Section id="opening" title="Opening">
      <Nodes.Greeting />
    </Section>
    <Section id="closing" title="Closing">
      <Nodes.NextSteps />
    </Section>
  </Document>,
);
```

Сохраните — и предпросмотр перезагрузится с новым узлом на месте.

## Данные приходят через `useState`

```tsx
import { Paragraph, useState } from "docxcelerate/template";
import type { DocumentData } from "../types.ts";

export const Greeting: Paragraph = () => {
  const [state] = useState((data: DocumentData) => ({
    name: data.recipientName,
  }));

  return <Paragraph id="greeting">Hello {state.name},</Paragraph>;
};
```

`useState` — это место, где данные входят в компонент, и единственное такое
место, так что от чего зависит узел, записано в одном объявлении, а не размазано
по коду, который это читает.

Инициализатор — обычный TypeScript. Языка шаблонов нет, поэтому условия,
форматирование и импорты просто доступны, а `if`, возвращающий другой абзац, — в
точности то, чем выглядит.

`Paragraph` называет и элемент, и тип компонента, поэтому
`const Greeting: Paragraph` говорит, что этот узел даёт, — а вернуть из него
`<Section>` будет ошибкой компиляции.

## Текст, который вы хотите получить сгенерированным

Некоторые абзацы нельзя написать заранее, потому что то, что они должны сказать,
зависит от человека, который их получит. Задайте вместо текста **промпты** и
**заполнитель**, чтобы предпросмотр оставался читаемым:

```tsx
export const TutorNote: Paragraph = () => {
  const [state] = useState((data: DocumentData) => ({
    applicantName: data.applicantName,
    interviewer: data.interviewer,
  }));

  useSetPrompts({
    generalPrompt: `Write two warm sentences about ${state.applicantName}'s interview.`,
  });
  useSetPlaceholders(`A note from ${state.interviewer}.`);

  return <Paragraph id="tutor-note" />;
};
```

Ничего объявлять не нужно, и режим выбирать тоже. Узел, у которого есть текст,
можно получить на вашей машине; узлу, у которого есть только промпты, нужен
движок. Компонент решает, чем он является, тем, что он задал; сборка это
выводит, а собранный пакет говорит, какие узлы движку предстоит разрешить.

Задать и то и другое на одном элементе — ошибка, а не подбрасывание монетки.
Промпты можно передать и пропсами, что читается лучше, когда они короткие, и
пропсы выигрывают у хука — вызывающая сторона может переопределить то, что
поставил вокруг общий хук.

Доступны пять слотов промптов. Обязателен только `generalPrompt`:

| Слот | Назначение |
| --- | --- |
| `generalPrompt` | Что узел должен сказать |
| `infoPrompt` | Контекст, который модель должна иметь, но не пересказывать |
| `negativePrompt` | Чего избегать |
| `systemPrompt` | Указания о роли и тоне |
| `examplePrompt` | Как выглядит хороший ответ, выписанный целиком |

К `examplePrompt` стоит обращаться в первую очередь. Если объяснять модели, какую
форму вы хотите, ей придётся догадываться; если показать готовую — ей будет что
повторить. Пример закрепляет то, что не должно меняться: как текст начинается, в
каком порядке идёт, какой он длины, насколько официально звучит. Модели остаётся
вписать только то, что и правда отличается от документа к документу. Пишите
пример как готовый текст, а не как бланк с пропусками, иначе вы снова описываете
форму.

`Image` и `Graph` работают так же: дайте `src` или `data` — и узел разрешится
локально; дайте промпты — и нет.

## Что показывает предпросмотр

Сборка для предпросмотра разрешает узлы с промптами в их **заполнители** и
никогда — в сгенерированный текст. Предпросмотр остаётся детерминированным и
бесплатным, так что вы можете дорабатывать структуру и оформление без единого
запроса, покидающего вашу машину.

Заполнитель — ещё и полезная дисциплина: если документ нечитаем с
заполнителями на месте, его структура делает слишком мало работы.

`usePlaceholderData` даёт вам подставные имена, даты и цифры, засеянные от места,
где стоит компонент, — тот же узел каждый раз показывает те же значения.
Предпросмотр, который перетасовывает себя при каждой сборке, никто не сможет
вычитать.

## Идентификаторы

id — это то, как движок адресует узел и как два артефакта сборки совмещаются в
диффе, поэтому считайте переименование ломающим изменением.

Его можно не указывать. Узел без id получает его из своего места, и это избавляет
ветвления и списки от требования придумывать имена. Чего нельзя — использовать
один дважды: два узла, претендующих на один id, — ошибка с указанием обеих
позиций, а не гонка, которую выигрывает последний.

## Куда дальше

- [Проекты документов](/ru/docs/document-projects/) — файлы вокруг ваших узлов
- [Документы и узлы](/ru/docs/essentials/documents-and-nodes/) — компонентная модель целиком, включая хуки
- [Модель узлов](/ru/docs/nodes/overview/) — каждый тип узла и предпросмотр для каждого
