gendesign/frontend/src/components/site-finder/analysis/ForecastConfidenceBlock.tsx
Light1YT 86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00

187 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* 6.3 Уровень уверенности — level pill + EXPLICIT drivers list from
* confidence.factors (DoD: «явный список drivers»).
*
* The factors map is mostly { note, level, value } entries, but the prod
* envelope also carries a stray boolean `advisory_capped` — narrowed out via a
* runtime type guard so we never render a non-factor.
*/
import { Badge } from "@/components/ui/Badge";
import type { ConfidenceFactor, ReportConfidence } from "@/types/forecast";
import { CONFIDENCE_RU, confidenceVariant } from "./forecast-helpers";
interface Props {
confidence: ReportConfidence;
}
// Human-readable labels for known factor keys; unknown keys fall back to the key.
const FACTOR_RU: Record<string, string> = {
deal_count: "Объём сделок",
analog_count: "Аналоги (ЖК)",
domrf_coverage: "Покрытие ДОМ.РФ",
history_months: "Глубина истории",
component: "Компонент",
};
function isConfidenceFactor(v: unknown): v is ConfidenceFactor {
if (typeof v !== "object" || v === null) return false;
const o = v as Record<string, unknown>;
return (
typeof o.note === "string" &&
(o.level === "high" || o.level === "medium" || o.level === "low")
);
}
function factorLabel(key: string): string {
if (FACTOR_RU[key]) return FACTOR_RU[key];
// component_2..N → «Компонент N» (note уже несёт «вкладывающий сервис: …»)
const m = key.match(/^component_(\d+)$/);
if (m) return `Компонент ${m[1]}`;
return key;
}
function formatFactorValue(value: number | null): string | null {
if (value == null) return null;
// Fractions in 0..1 read as percent (e.g. coverage); integers/large stay raw.
// Upper bound is inclusive so full coverage (1.0) renders as «100%», not «1».
if (value > 0 && value <= 1) return `${Math.round(value * 100)}%`;
return Math.round(value).toLocaleString("ru");
}
export function ForecastConfidenceBlock({ confidence }: Props) {
const level = confidence.level;
const drivers = Object.entries(confidence.factors)
.filter((entry): entry is [string, ConfidenceFactor] =>
isConfidenceFactor(entry[1]),
)
// Sort high → medium → low so the strongest evidence reads first.
.sort((a, b) => LEVEL_RANK[a[1].level] - LEVEL_RANK[b[1].level]);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{/* Level pill + rationale */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span
style={{
fontSize: 11,
fontWeight: 500,
letterSpacing: "0.04em",
textTransform: "uppercase",
color: "var(--fg-secondary)",
}}
>
Уровень уверенности
</span>
{level ? (
<Badge variant={confidenceVariant(level)} size="md">
{CONFIDENCE_RU[level]}
</Badge>
) : (
<span style={{ fontSize: 13, color: "var(--fg-tertiary)" }}>
не определена
</span>
)}
</div>
{confidence.rationale && (
<p
style={{
margin: 0,
fontSize: 13,
lineHeight: 1.5,
color: "var(--fg-secondary)",
}}
>
{confidence.rationale}
</p>
)}
</div>
{/* Explicit drivers list */}
{drivers.length > 0 ? (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<span
style={{
fontSize: 11,
fontWeight: 500,
letterSpacing: "0.04em",
textTransform: "uppercase",
color: "var(--fg-secondary)",
}}
>
Факторы достоверности ({drivers.length})
</span>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{drivers.map(([key, factor]) => {
const valueStr = formatFactorValue(factor.value);
return (
<div
key={key}
style={{
display: "flex",
alignItems: "flex-start",
gap: 8,
padding: "8px 12px",
background: "var(--bg-card-alt)",
border: "1px solid var(--border-soft)",
borderRadius: 8,
}}
>
<Badge variant={confidenceVariant(factor.level)} size="sm">
{CONFIDENCE_RU[factor.level]}
</Badge>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "var(--fg-primary)",
}}
>
{factorLabel(key)}
{valueStr != null && (
<span
style={{
marginLeft: 6,
fontWeight: 400,
color: "var(--fg-secondary)",
fontVariantNumeric: "tabular-nums",
}}
>
· {valueStr}
</span>
)}
</div>
<div
style={{
fontSize: 12,
color: "var(--fg-tertiary)",
marginTop: 2,
}}
>
{factor.note}
</div>
</div>
</div>
);
})}
</div>
</div>
) : (
<p style={{ fontSize: 13, color: "var(--fg-tertiary)", margin: 0 }}>
Факторы достоверности недоступны.
</p>
)}
</div>
);
}
const LEVEL_RANK: Record<ConfidenceFactor["level"], number> = {
high: 0,
medium: 1,
low: 2,
};