Per-biomarker UI компонент в bloodgpt-frontend (.NET stack, legacy track). Описывает как тренд представлен как первоклассная сущность внутри параметра — отдельный объект с двумя ребёнками: raw points + AI-generated prose. Это шаблон, который variant feat/actuality-mvp-dirty пока полностью не повторил для patient-scoped пути (carry-over Артура от 2026-05-18); описание ниже — снимок legacy дизайна, на который имеет смысл опираться при проектировании нового API shape (normal-biomarker-pipeline-coverage).

Data shape (data model)

В legacy DTO ([packages/sdk/generated/api/index.ts]1) тренд — поле trend?: TrendAnalysisDto внутри ParameterInfoDto:

interface ParameterInfoDto {
  // identity + текущее значение
  id, name, resultValue, units, normalMin/Max, borderlineMin/Max,
  isAbnormal, isCritical, isHealthy, status,
  description,                  // base описание биомаркера (per-биомаркер, static)
 
  // trending metadata
  trendingGroupId,              // ключ группировки исторических LOINC-ренеймов
  trendCount,                   // сколько исторических значений существует
 
  trend?: TrendAnalysisDto {
    parameterName,
    trendingGroupId,
    parameterV2Id,
    trendAnalysis,              // AI-generated markdown prose
    trendData: {
      "2023-01-15": TrendDataPointDto { value, isAbnormal, ... },
      "2024-03-20": TrendDataPointDto { value, isAbnormal, ... },
      ...
    }
  }
}

Ключевая структура — trend это самостоятельный объект на одном уровне детализации с параметром. Series (trendData, key=ISO-date) и prose (trendAnalysis, markdown) живут в одном месте, передаются вместе. trendingGroupId дублируется на уровне параметра — для определения «нужно ли вообще тренд-секцию показывать» без необходимости заглядывать внутрь trend. См. trending-groups про сам механизм группировки.

В .NET stack данные тренда лежат в Postgres (LoincParameters.TrendingGroupId колонка), prose trendAnalysis сохраняется отдельной mutation’ой и редактируется. FHIR в этом stack’е не используется.

UI panel — что отображает

Компонент ParamPanelTrendText.tsx2 (491 строка) рендерит TEXT часть тренда:

┌─ Trend Analysis ────────────────────────────────┐ ◄ Edit pencil (doctor only)
│  [trendAnalysis as markdown — AI prose 3-5      │
│   предложений про изменение значений per        │
│   биомаркер; рендерится через MarkdownRenderer]│
├─────────────────────────────────────────────────┤
│  [description — base biomarker description,    │
│   что вообще значит этот биомаркер]             │
├─ Read more / Show less ───────── Learn more ────┤
└─────────────────────────────────────────────────┘

Chart (точки на графике) — отдельный компонент ResultsTrendChartJs.tsx3, рендерится в той же ParamPanel.tsx выше или рядом. Текст и chart не дублируют данные — chart использует trend.trendData, текст использует trend.trendAnalysis.

Режимы рендера

СценарийЧто показывается
trendCount ≤ 1 (один тест)Только description + Learn More кнопка. Trend секция полностью скрывается через флаг hideTrendAnalysis=true
trendCount > 1, hasTrendDataHeader «Trend Analysis» + markdown trendAnalysis + border + description. Read more / Show less переключение если текст overflow’ит 120px (COLLAPSED_TEXT_HEIGHT_PX)
trendLoadingSpinner + «Loading trend analytics…»
trendErrorInfo icon + «Trends require multiple tests»
!hasTrendDataНе рендерится (return null)
Doctor editingEdit pencil → Textarea-режим для обоих полей (trendAnalysis + description) → Save/Cancel buttons, сохраняется через useTrendsUpdateTrendAnalysis mutation
PDF modeВсегда expanded, без toggle buttons, более компактные размеры (text-[13px] leading-[1.45])
Comparison variantSide-by-side рендер для сравнения двух тестов

«Single-value mode» — единственный путь, где description (base описание биомаркера) показывается без trend-блока. В multi-value mode они идут вместе, разделённые border.

Lazy fetch

Тренд не тянется eagerly для всех биомаркеров на странице — это было бы дорого при типичных 20-50 параметрах на тест. Вместо этого ParamPanel.tsx4 использует useLazyTrendAnalysis hook:

const canFetchTrend =
  !isSingleValueMode &&
  !trendsPending &&
  Boolean(paramData.trendingGroupId);
 
const { trendData, trendLoading, trendError } = useLazyTrendAnalysis({
  enabled: canFetchTrend,
  initialTrendData: data.trend ?? null,    // pre-loaded из ParameterInfoDto
  trendingGroupId: paramData.trendingGroupId,
  refreshKey: trendRefreshNonce,
});

enabled гейт основан на:

  • !isSingleValueMode — для одного теста запрос бессмыслен
  • !trendsPending — pending-флаг с уровня страницы (когда analysis ещё бежит)
  • trendingGroupId !== null — нужен ключ группировки

initialTrendData — если ParameterInfoDto.trend уже пришёл в начальной выборке, hook стартует с этим значением без fetch’а. Это позволяет сервер-side оптимально pre-fetch’ить тренды для важных биомаркеров (или всех) и доставлять inline.

refreshKey — invalidation token, увеличивается когда юзер кликнул «Refresh analysis» или сохранил edit prose — заставляет re-fetch.

Editing affordance — doctor/org-admin only

Edit pencil показывается если canShowEditButton === true, что зависит от role guard. Когда юзер кликает:

  1. Switch текстовые блоки на <Textarea> (рисует drafts descriptionDraft / trendAnalysisDraft)
  2. Show Save / Cancel buttons
  3. На Save — mutation useTrendsUpdateTrendAnalysis обновляет prose в backend’е
  4. Optimistic update / refresh

Это значит trendAnalysis не immutable AI-output, а editable artifact — врач может corrigировать AI-prose, и эти правки сохраняются в legacy backend per биомаркер. Это важная особенность legacy track’а — separation между «AI первый draft» и «human-validated final».

Открытые вопросы

  • FHIR equivalent для editable trendAnalysis. В legacy DB поле LoincParameters.TrendingGroupId хранит ID, отдельная таблица хранит editable prose. В FHIR-стэке editable AI-content concept ещё не зафиксирован — открытый вопрос пересекается с fhir-modeling-ai-content и fhir-resource-origin-and-lifecycle (надо ли стampить meta.tag origin=human-edited после правки врача).
  • Где живёт AI trend prose в новом patient-scoped pipeline. Сегодня — нигде надёжно: Reasoner получает history плохо (поле даты пропало после Python→TS миграции), Personalizer имеет пустой formatHistory(hist) слот. Артур взял carry-over на пересмотр (2026-05-18 созвон). См. biomarker-analysis-pipeline.
  • Cross-biomarker trends в Insights секции. Per-биомаркер тренд — это микро-tour; макро-обзор («3 биомаркера ухудшаются», «холестерин стабильно растёт два года») — это уже уровень Insights, не парам-панели. См. Phase 3 в health-report-pipeline (5 Insight секций, не реализованы).
  • Lazy fetch vs eager в FHIR-стэке. В legacy lazy через trendingGroupId. В FHIR’е raw series тянется через Observation?subject=X&code=LOINC history запрос — может быть медленнее, может потребоваться precomputation в response shape для /api/biomarkers чтобы избежать N+1 fetch’ей в UI.
  • Single-value mode triggering. trendCount ≤ 1 сегодня скрывает trend секцию полностью. В новой архитектуре с patient-scoped views (агрегация всех тестов пациента) trendCount всегда >1 для повторно сданного биомаркера — single-value mode может стать deprecated кроме first-upload UX.

Связано

  • trending-groups — механизм группировки LOINC-кодов одного analyte; источник trendingGroupId ключа
  • biomarker — entity-уровень
  • loinc — base codification поверх которого живут trending groups
  • biomarker-analysis-pipeline — где AI trend prose должна генериться в новом стэке (Reasoner + Personalizer, сегодня carry-over)
  • normal-biomarker-pipeline-coverage — variant B миграция; trend pass-through через Reasoner — отдельный workstream
  • health-report-pipeline — Phase 3 Insight секции включают cross-biomarker «Trends» как одна из 5 секций
  • fhir-modeling-ai-content — общий концепт mapping AI-output в FHIR; editable trendAnalysis — частный случай
  • fhir-resource-origin-and-lifecyclemeta.tag стратегия для human-edited AI prose

Сноски

  1. packages/sdk/generated/api/index.tsTrendAnalysisDto, ParameterInfoDto.trend? field, generated from .NET backend OpenAPI schema, accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-frontend/blob/main/packages/sdk/generated/api/index.ts.

  2. apps/dashboard/components/features/Results/ParamPanel/ParamPanelTrendText.tsx — UI компонент trend prose рендера, 491 строка, accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-frontend/blob/main/apps/dashboard/components/features/Results/ParamPanel/ParamPanelTrendText.tsx.

  3. apps/dashboard/components/charts/ResultsTrendChartJs.tsx — chart-side рендер для trendData points, accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-frontend/blob/main/apps/dashboard/components/charts/ResultsTrendChartJs.tsx.

  4. apps/dashboard/components/features/Results/ParamPanel/ParamPanel.tsx — orchestrator, использует useLazyTrendAnalysis (стр. 139-152), useTrendsUpdateTrendAnalysis mutation для edit (стр. 121), accessed 2026-05-18, https://github.com/Realai-plus/bloodgpt-frontend/blob/main/apps/dashboard/components/features/Results/ParamPanel/ParamPanel.tsx.