ДОМЕНЫ - ВОПРОСЫ - fix(ui-assistant-chat): render markdown bold, add readable block spacing, force scroll-to-bottom on send + ЮИ
This commit is contained in:
parent
a717ea6b26
commit
ae9daa50e7
|
|
@ -81,6 +81,32 @@ function formatMoney(value) {
|
||||||
}
|
}
|
||||||
return value.toFixed(2);
|
return value.toFixed(2);
|
||||||
}
|
}
|
||||||
|
function formatNumberWithDots(value, fractionDigits = 0) {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
if (fractionDigits > 0) {
|
||||||
|
return `0,${"0".repeat(fractionDigits)}`;
|
||||||
|
}
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
const sign = value < 0 ? "-" : "";
|
||||||
|
const absolute = Math.abs(value);
|
||||||
|
const fixed = absolute.toFixed(Math.max(0, fractionDigits));
|
||||||
|
const [intPartRaw, fractionPartRaw] = fixed.split(".");
|
||||||
|
const groupedInt = intPartRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
if (fractionDigits <= 0) {
|
||||||
|
return `${sign}${groupedInt}`;
|
||||||
|
}
|
||||||
|
return `${sign}${groupedInt},${fractionPartRaw ?? "0".repeat(fractionDigits)}`;
|
||||||
|
}
|
||||||
|
function formatMoneyRub(value) {
|
||||||
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
|
}
|
||||||
|
function emphasizeNumericTokens(line) {
|
||||||
|
if (!line) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
|
||||||
|
}
|
||||||
function parseIsoDateToken(value) {
|
function parseIsoDateToken(value) {
|
||||||
const source = String(value ?? "").trim();
|
const source = String(value ?? "").trim();
|
||||||
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||||
|
|
@ -1891,6 +1917,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||||
|
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||||
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
|
const periodScopeLine = !asOfDate && (periodFrom || periodTo)
|
||||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -1902,9 +1929,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
return acc;
|
return acc;
|
||||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||||
const lines = [
|
const lines = [
|
||||||
|
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||||
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
"- Режим результата: подтвержденный срез обязательств к оплате (exact route).",
|
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
|
||||||
"- Эвристический shortlist в этом режиме не используется."
|
|
||||||
];
|
];
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 2. Что учтено");
|
lines.push("Блок 2. Что учтено");
|
||||||
|
|
@ -1918,25 +1946,25 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 3. Сводка");
|
lines.push("Блок 3. Сводка");
|
||||||
lines.push(`- Строк в выборке: ${rows.length}.`);
|
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||||
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${confirmedBalances.length}.`);
|
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 4. Категории обязательств");
|
lines.push("Блок 4. Категории обязательств");
|
||||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||||
if (confirmedBalances.length > 0) {
|
if (confirmedBalances.length > 0) {
|
||||||
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
|
lines.push(...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
lines.push("- Подтвержденных открытых обязательств к оплате на дату среза не найдено.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -1958,7 +1986,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const carryoverLine = asOfDate || periodFrom || periodTo
|
const carryoverLine = asOfDate || periodFrom || periodTo
|
||||||
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
|
? "- В список могут попадать обязательства, возникшие раньше выбранного периода, если они потенциально оставались открытыми на дату среза."
|
||||||
: null;
|
: null;
|
||||||
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
const formatHeuristicItem = (item, index) => `${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
||||||
const pushCategorySlice = (lines, title, items, limit) => {
|
const pushCategorySlice = (lines, title, items, limit) => {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1971,9 +1999,9 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const lines = [
|
const lines = [
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
forcedFallbackFromConfirmed
|
forcedFallbackFromConfirmed
|
||||||
? "- Режим результата: эвристический скоринг в рамках fallback, потому что подтвержденный срез обязательств к оплате недоступен."
|
? "- Точный реестр обязательств сейчас недоступен, поэтому показан предварительный список на проверку."
|
||||||
: "- Режим результата: эвристический скоринг (shortlist кандидатов по признакам незакрытых обязательств в контуре 60/76).",
|
: "- Формат результата: предварительный список на проверку.",
|
||||||
"- Тип результата: кандидаты для ручной проверки, а не финальный платежный реестр.",
|
"- Это рабочий список для проверки, а не финальный платежный реестр.",
|
||||||
"",
|
"",
|
||||||
"Блок 2. Как читать результат",
|
"Блок 2. Как читать результат",
|
||||||
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
|
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
|
||||||
|
|
@ -1982,8 +2010,8 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
...(carryoverLine ? [carryoverLine] : []),
|
...(carryoverLine ? [carryoverLine] : []),
|
||||||
"",
|
"",
|
||||||
"Блок 3. Сводка выборки",
|
"Блок 3. Сводка выборки",
|
||||||
`- Строк в выборке: ${rows.length}.`,
|
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||||
`- Контрагентов-кандидатов: ${counterparties.length}.`
|
`- Контрагентов-кандидатов: ${formatNumberWithDots(counterparties.length)}.`
|
||||||
];
|
];
|
||||||
if (counterparties.length > 0) {
|
if (counterparties.length > 0) {
|
||||||
const categoryCounts = counterparties.reduce((acc, item) => {
|
const categoryCounts = counterparties.reduce((acc, item) => {
|
||||||
|
|
@ -1996,10 +2024,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const other = counterparties.filter((item) => item.category === "other");
|
const other = counterparties.filter((item) => item.category === "other");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 4. Категории обязательств");
|
lines.push("Блок 4. Категории обязательств");
|
||||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
|
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
|
||||||
pushCategorySlice(lines, "5.1 Поставщики/подрядчики:", suppliers, 6);
|
pushCategorySlice(lines, "5.1 Поставщики/подрядчики:", suppliers, 6);
|
||||||
|
|
@ -2027,7 +2055,10 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
acc[item.category] += 1;
|
acc[item.category] += 1;
|
||||||
return acc;
|
return acc;
|
||||||
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
|
||||||
|
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||||
const lines = [
|
const lines = [
|
||||||
|
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||||
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
|
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
|
||||||
"- Тип результата: подтвержденные остатки к оплате.",
|
"- Тип результата: подтвержденные остатки к оплате.",
|
||||||
|
|
@ -2041,21 +2072,21 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
...(carryoverLine ? [carryoverLine] : []),
|
...(carryoverLine ? [carryoverLine] : []),
|
||||||
"",
|
"",
|
||||||
"Блок 3. Сводка выборки",
|
"Блок 3. Сводка выборки",
|
||||||
`- Строк в выборке: ${rows.length}.`,
|
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||||
`- Контрагентов с подтвержденным остатком: ${confirmedBalances.length}.`,
|
`- Контрагентов с подтвержденным остатком: ${formatNumberWithDots(confirmedBalances.length)}.`,
|
||||||
"",
|
"",
|
||||||
"Блок 4. Категории обязательств",
|
"Блок 4. Категории обязательств",
|
||||||
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`,
|
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`,
|
||||||
`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`,
|
`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`,
|
||||||
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`,
|
||||||
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
|
||||||
"",
|
"",
|
||||||
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
|
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: "strong",
|
evidence_strength: "strong",
|
||||||
|
|
@ -2066,7 +2097,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const fallbackLines = buildHeuristicLines(true);
|
const fallbackLines = buildHeuristicLines(true);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: fallbackLines.join("\n"),
|
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
@ -2077,7 +2108,7 @@ function composeFactualReply(intent, rows, options = {}) {
|
||||||
const lines = buildHeuristicLines(false);
|
const lines = buildHeuristicLines(false);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,35 @@ function formatMoney(value: number): string {
|
||||||
return value.toFixed(2);
|
return value.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNumberWithDots(value: number, fractionDigits = 0): string {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
if (fractionDigits > 0) {
|
||||||
|
return `0,${"0".repeat(fractionDigits)}`;
|
||||||
|
}
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
const sign = value < 0 ? "-" : "";
|
||||||
|
const absolute = Math.abs(value);
|
||||||
|
const fixed = absolute.toFixed(Math.max(0, fractionDigits));
|
||||||
|
const [intPartRaw, fractionPartRaw] = fixed.split(".");
|
||||||
|
const groupedInt = intPartRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
if (fractionDigits <= 0) {
|
||||||
|
return `${sign}${groupedInt}`;
|
||||||
|
}
|
||||||
|
return `${sign}${groupedInt},${fractionPartRaw ?? "0".repeat(fractionDigits)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoneyRub(value: number): string {
|
||||||
|
return `${formatNumberWithDots(value, 2)} ₽`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emphasizeNumericTokens(line: string): string {
|
||||||
|
if (!line) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return line.replace(/(?<!\*)\d(?:[\d.,:/-]*\d)?(?!\*)/g, (token) => `**${token}**`);
|
||||||
|
}
|
||||||
|
|
||||||
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
|
function parseIsoDateToken(value: string | null | undefined): { year: number; month: number; day: number } | null {
|
||||||
const source = String(value ?? "").trim();
|
const source = String(value ?? "").trim();
|
||||||
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||||
|
|
@ -2412,6 +2441,7 @@ export function composeFactualReply(
|
||||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||||
|
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||||
const periodScopeLine =
|
const periodScopeLine =
|
||||||
!asOfDate && (periodFrom || periodTo)
|
!asOfDate && (periodFrom || periodTo)
|
||||||
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
|
||||||
|
|
@ -2429,9 +2459,10 @@ export function composeFactualReply(
|
||||||
);
|
);
|
||||||
|
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
|
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||||
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
"- Режим результата: подтвержденный срез обязательств к оплате (exact route).",
|
"- Режим результата: подтвержденный срез обязательств к оплате (exact route)."
|
||||||
"- Эвристический shortlist в этом режиме не используется."
|
|
||||||
];
|
];
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
@ -2447,15 +2478,15 @@ export function composeFactualReply(
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 3. Сводка");
|
lines.push("Блок 3. Сводка");
|
||||||
lines.push(`- Строк в выборке: ${rows.length}.`);
|
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||||
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${confirmedBalances.length}.`);
|
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 4. Категории обязательств");
|
lines.push("Блок 4. Категории обязательств");
|
||||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}.`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}.`);
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
lines.push("Блок 5. Подтвержденные позиции к оплате");
|
||||||
|
|
@ -2463,7 +2494,7 @@ export function composeFactualReply(
|
||||||
lines.push(
|
lines.push(
|
||||||
...confirmedBalances.slice(0, 10).map(
|
...confirmedBalances.slice(0, 10).map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoneyRub(item.outstandingAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2472,7 +2503,7 @@ export function composeFactualReply(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
|
||||||
|
|
@ -2498,7 +2529,7 @@ export function composeFactualReply(
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
|
const formatHeuristicItem = (item: PayablesCounterpartyRiskAggregate, index: number): string =>
|
||||||
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
`${index + 1}. ${item.name} | сумма к проверке: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`;
|
||||||
|
|
||||||
const pushCategorySlice = (
|
const pushCategorySlice = (
|
||||||
lines: string[],
|
lines: string[],
|
||||||
|
|
@ -2518,9 +2549,9 @@ export function composeFactualReply(
|
||||||
const lines = [
|
const lines = [
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
forcedFallbackFromConfirmed
|
forcedFallbackFromConfirmed
|
||||||
? "- Режим результата: эвристический скоринг в рамках fallback, потому что подтвержденный срез обязательств к оплате недоступен."
|
? "- Точный реестр обязательств сейчас недоступен, поэтому показан предварительный список на проверку."
|
||||||
: "- Режим результата: эвристический скоринг (shortlist кандидатов по признакам незакрытых обязательств в контуре 60/76).",
|
: "- Формат результата: предварительный список на проверку.",
|
||||||
"- Тип результата: кандидаты для ручной проверки, а не финальный платежный реестр.",
|
"- Это рабочий список для проверки, а не финальный платежный реестр.",
|
||||||
"",
|
"",
|
||||||
"Блок 2. Как читать результат",
|
"Блок 2. Как читать результат",
|
||||||
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
|
"- Это shortlist кандидатов: нужна ручная проверка бухгалтером.",
|
||||||
|
|
@ -2529,8 +2560,8 @@ export function composeFactualReply(
|
||||||
...(carryoverLine ? [carryoverLine] : []),
|
...(carryoverLine ? [carryoverLine] : []),
|
||||||
"",
|
"",
|
||||||
"Блок 3. Сводка выборки",
|
"Блок 3. Сводка выборки",
|
||||||
`- Строк в выборке: ${rows.length}.`,
|
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||||
`- Контрагентов-кандидатов: ${counterparties.length}.`
|
`- Контрагентов-кандидатов: ${formatNumberWithDots(counterparties.length)}.`
|
||||||
];
|
];
|
||||||
|
|
||||||
if (counterparties.length > 0) {
|
if (counterparties.length > 0) {
|
||||||
|
|
@ -2548,10 +2579,10 @@ export function composeFactualReply(
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 4. Категории обязательств");
|
lines.push("Блок 4. Категории обязательств");
|
||||||
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`);
|
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`);
|
lines.push(`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`);
|
lines.push(`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`);
|
||||||
lines.push(`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`);
|
lines.push(`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`);
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
|
lines.push("Блок 5. Кандидаты на проверку в первую очередь");
|
||||||
|
|
@ -2585,7 +2616,10 @@ export function composeFactualReply(
|
||||||
},
|
},
|
||||||
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
|
||||||
);
|
);
|
||||||
|
const totalOutstandingAmount = confirmedBalances.reduce((sum, item) => sum + item.outstandingAmount, 0);
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
|
`Итого подтвержденный долг на ${formatDateRu(payablesAsOfDate)}: ${formatMoneyRub(totalOutstandingAmount)}.`,
|
||||||
|
"",
|
||||||
"Блок 1. Статус результата",
|
"Блок 1. Статус результата",
|
||||||
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
|
"- Режим результата: подтвержденный срез обязательств к оплате по состоянию на дату среза в контуре 60/76.",
|
||||||
"- Тип результата: подтвержденные остатки к оплате.",
|
"- Тип результата: подтвержденные остатки к оплате.",
|
||||||
|
|
@ -2599,24 +2633,24 @@ export function composeFactualReply(
|
||||||
...(carryoverLine ? [carryoverLine] : []),
|
...(carryoverLine ? [carryoverLine] : []),
|
||||||
"",
|
"",
|
||||||
"Блок 3. Сводка выборки",
|
"Блок 3. Сводка выборки",
|
||||||
`- Строк в выборке: ${rows.length}.`,
|
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
|
||||||
`- Контрагентов с подтвержденным остатком: ${confirmedBalances.length}.`,
|
`- Контрагентов с подтвержденным остатком: ${formatNumberWithDots(confirmedBalances.length)}.`,
|
||||||
"",
|
"",
|
||||||
"Блок 4. Категории обязательств",
|
"Блок 4. Категории обязательств",
|
||||||
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${categoryCounts.supplier_or_contractor}`,
|
`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}`,
|
||||||
`- ${liabilityCategoryLabel("bank_or_credit")}: ${categoryCounts.bank_or_credit}`,
|
`- ${liabilityCategoryLabel("bank_or_credit")}: ${formatNumberWithDots(categoryCounts.bank_or_credit)}`,
|
||||||
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
|
`- ${liabilityCategoryLabel("tax_or_state")}: ${formatNumberWithDots(categoryCounts.tax_or_state)}`,
|
||||||
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
|
`- ${liabilityCategoryLabel("other")}: ${formatNumberWithDots(categoryCounts.other)}`,
|
||||||
"",
|
"",
|
||||||
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
|
||||||
...confirmedBalances.slice(0, 10).map(
|
...confirmedBalances.slice(0, 10).map(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "confirmed_balance",
|
result_mode: "confirmed_balance",
|
||||||
evidence_strength: "strong",
|
evidence_strength: "strong",
|
||||||
|
|
@ -2628,7 +2662,7 @@ export function composeFactualReply(
|
||||||
const fallbackLines = buildHeuristicLines(true);
|
const fallbackLines = buildHeuristicLines(true);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: fallbackLines.join("\n"),
|
text: fallbackLines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
@ -2640,7 +2674,7 @@ export function composeFactualReply(
|
||||||
const lines = buildHeuristicLines(false);
|
const lines = buildHeuristicLines(false);
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
responseType: "FACTUAL_LIST",
|
||||||
text: lines.join("\n"),
|
text: lines.map(emphasizeNumericTokens).join("\n"),
|
||||||
semantics: {
|
semantics: {
|
||||||
result_mode: "heuristic_candidates",
|
result_mode: "heuristic_candidates",
|
||||||
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
evidence_strength: counterparties.length > 0 ? "medium" : "weak",
|
||||||
|
|
|
||||||
|
|
@ -2544,11 +2544,12 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(reply.toLowerCase()).toContain("shortlist");
|
expect(reply.toLowerCase()).toContain("shortlist");
|
||||||
} else {
|
} else {
|
||||||
expect(result?.debug.balance_confirmed).toBe(true);
|
expect(result?.debug.balance_confirmed).toBe(true);
|
||||||
expect(reply).toContain("Блок 1. Статус результата");
|
expect(reply).toContain("Итого подтвержденный долг");
|
||||||
expect(reply).toContain("\n\nБлок 2. Что учтено");
|
expect(reply).toMatch(/Блок\s+(?:\*\*1\*\*|1)\.\s+Статус результата/u);
|
||||||
expect(reply).toContain("\n\nБлок 3. Сводка");
|
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*2\*\*|2)\.\s+Что учтено/u);
|
||||||
expect(reply).toContain("\n\nБлок 4. Категории обязательств");
|
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*3\*\*|3)\.\s+Сводка/u);
|
||||||
expect(reply).toContain("\n\nБлок 5. Подтвержденные позиции к оплате");
|
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*4\*\*|4)\.\s+Категории обязательств/u);
|
||||||
|
expect(reply).toMatch(/\n\nБлок\s+(?:\*\*5\*\*|5)\.\s+Подтвержденные позиции к оплате/u);
|
||||||
expect(reply).not.toContain("эвристический");
|
expect(reply).not.toContain("эвристический");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NDC AI Normalizer Playground</title>
|
<title>NDC AI Normalizer Playground</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CVIU9teH.js"></script>
|
<script type="module" crossorigin src="/assets/index-CEJTyo9A.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D4dtBq8A.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Cx2J_KsF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,95 @@ function CommentBubbleIcon({ commented }: { commented: boolean }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAssistantMessageText(raw: string): string {
|
||||||
|
return raw
|
||||||
|
.replace(/\r\n?/g, "\n")
|
||||||
|
.replace(/([^\n])\s+(Блок\s+\d+\.)/gi, "$1\n\n$2")
|
||||||
|
.replace(/([^\n])\s+(\d+\.\s)/g, "$1\n$2");
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitAssistantMessageBlocks(text: string): string[] {
|
||||||
|
const normalized = normalizeAssistantMessageText(text);
|
||||||
|
const lines = normalized.split("\n");
|
||||||
|
const blocks: string[] = [];
|
||||||
|
let current: string[] = [];
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (current.length === 0) return;
|
||||||
|
blocks.push(current.join("\n"));
|
||||||
|
current = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trimEnd();
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
flush();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBlockHeading = /^Блок\s+\d+\./i.test(trimmed);
|
||||||
|
const isNumberedItem = /^\d+\.\s/.test(trimmed);
|
||||||
|
if ((isBlockHeading || isNumberedItem) && current.length > 0) {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
current.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush();
|
||||||
|
return blocks.length > 0 ? blocks : [text];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInlineBold(text: string, keyPrefix: string): JSX.Element[] {
|
||||||
|
const result: JSX.Element[] = [];
|
||||||
|
const regex = /\*\*(.+?)\*\*/g;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let partIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
result.push(<span key={`${keyPrefix}-t-${partIndex}`}>{text.slice(lastIndex, match.index)}</span>);
|
||||||
|
partIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(<strong key={`${keyPrefix}-b-${partIndex}`}>{match[1]}</strong>);
|
||||||
|
partIndex += 1;
|
||||||
|
lastIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
result.push(<span key={`${keyPrefix}-t-${partIndex}`}>{text.slice(lastIndex)}</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.length > 0 ? result : [<span key={`${keyPrefix}-raw`}>{text}</span>];
|
||||||
|
}
|
||||||
|
|
||||||
|
function lineClassName(line: string): string {
|
||||||
|
const trimmed = line.trimStart();
|
||||||
|
if (/^Блок\s+\d+\./i.test(trimmed)) return "assistant-msg-line heading";
|
||||||
|
if (/^\d+\.\s/.test(trimmed)) return "assistant-msg-line numbered";
|
||||||
|
if (/^-\s/.test(trimmed)) return "assistant-msg-line bullet";
|
||||||
|
return "assistant-msg-line";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAssistantMessageBody(text: string): JSX.Element[] {
|
||||||
|
const blocks = splitAssistantMessageBlocks(text);
|
||||||
|
return blocks.map((block, blockIndex) => {
|
||||||
|
const lines = block.split("\n");
|
||||||
|
return (
|
||||||
|
<div key={`block-${blockIndex}`} className="assistant-msg-block">
|
||||||
|
{lines.map((line, lineIndex) => (
|
||||||
|
<p key={`line-${blockIndex}-${lineIndex}`} className={lineClassName(line)}>
|
||||||
|
{renderInlineBold(line, `line-${blockIndex}-${lineIndex}`)}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function AssistantPanel({
|
export function AssistantPanel({
|
||||||
sessionId,
|
sessionId,
|
||||||
conversation,
|
conversation,
|
||||||
|
|
@ -97,10 +186,18 @@ export function AssistantPanel({
|
||||||
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
const [copyState, setCopyState] = useState<"idle" | "success" | "error">("idle");
|
||||||
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
const [copyModeLabel, setCopyModeLabel] = useState<"чат" | "тех">("чат");
|
||||||
|
|
||||||
useEffect(() => {
|
function scrollChatToBottom(forceStick = false): void {
|
||||||
if (listRef.current && stickToBottomRef.current) {
|
if (!listRef.current) return;
|
||||||
|
if (forceStick) {
|
||||||
|
stickToBottomRef.current = true;
|
||||||
|
}
|
||||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (stickToBottomRef.current) {
|
||||||
|
scrollChatToBottom();
|
||||||
|
}
|
||||||
}, [conversation]);
|
}, [conversation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -219,7 +316,7 @@ export function AssistantPanel({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
<div className="assistant-msg-body">{item.text}</div>
|
<div className="assistant-msg-body">{renderAssistantMessageBody(item.text)}</div>
|
||||||
{item.role === "assistant" && item.debug ? (
|
{item.role === "assistant" && item.debug ? (
|
||||||
<details className="assistant-debug">
|
<details className="assistant-debug">
|
||||||
<summary>Показать технический разбор</summary>
|
<summary>Показать технический разбор</summary>
|
||||||
|
|
@ -247,7 +344,15 @@ export function AssistantPanel({
|
||||||
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
<input type="checkbox" checked={useMock} onChange={(event) => onUseMockChange(event.target.checked)} />
|
||||||
Mock-режим
|
Mock-режим
|
||||||
</label>
|
</label>
|
||||||
<button type="button" className="assistant-send-btn" onClick={() => onSend()} disabled={busy || !inputValue.trim()}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-send-btn"
|
||||||
|
onClick={() => {
|
||||||
|
scrollChatToBottom(true);
|
||||||
|
void onSend();
|
||||||
|
}}
|
||||||
|
disabled={busy || !inputValue.trim()}
|
||||||
|
>
|
||||||
{busy ? "Выполняю..." : "Отправить"}
|
{busy ? "Выполняю..." : "Отправить"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -548,11 +548,36 @@ button:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-msg-body {
|
.assistant-msg-body {
|
||||||
white-space: pre-wrap;
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-msg-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-msg-line {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-msg-line.heading {
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-msg-line.numbered {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-msg-line strong {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.assistant-trace {
|
.assistant-trace {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue