-
-
-
-
-
-
-
-
@@ -586,10 +572,6 @@
-
-
-
-
@@ -604,11 +586,11 @@
| # |
- Наименование товара/услуги |
+ Наименование (raw) |
+ Норм. наименование |
Регион |
Ед. изм. |
- Кол-во |
- Дата закупки |
+ ID контракта |
@@ -633,27 +615,17 @@
-
+
-
-
Коэффициент пересчёта
@@ -688,34 +658,6 @@
-
-
-
-
-
-
-
@@ -733,22 +675,7 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
});
// ═══ TAB 1: Search ═══
-const DEMO_DATA = [
- { name: 'Бумага офисная А4, 80 г/м², 500 листов', region: 'Москва', unit: 'упак', qty: 500, date: '2026-03-15' },
- { name: 'Бумага писчая А4 «Снегурочка»', region: 'Санкт-Петербург', unit: 'упак', qty: 200, date: '2026-03-10' },
- { name: 'Бумага для принтера А4, класс B', region: 'Новосибирская область', unit: 'упак', qty: 1000, date: '2026-02-28' },
- { name: 'Бумага офисная А3, 80 г/м²', region: 'Краснодарский край', unit: 'упак', qty: 150, date: '2026-02-20' },
- { name: 'Бумага для копировальных аппаратов А4', region: 'Москва', unit: 'шт', qty: 3000, date: '2026-02-14' },
- { name: 'Бумага SvetoCopy А4 500 л.', region: 'Свердловская область', unit: 'упак', qty: 800, date: '2026-01-30' },
- { name: 'Бумага мелованная А4 для лазерной печати', region: 'Республика Татарстан', unit: 'упак', qty: 50, date: '2026-01-22' },
- { name: 'Бумага офисная А4 «Ballet Classic»', region: 'Москва', unit: 'упак', qty: 2400, date: '2026-01-15' },
- { name: 'Бумага цветная А4, 10 цветов', region: 'Санкт-Петербург', unit: 'упак', qty: 120, date: '2026-01-08' },
- { name: 'Бумага для заметок 76×76 мм, жёлтая', region: 'Краснодарский край', unit: 'шт', qty: 5000, date: '2025-12-29' },
- { name: 'Бумага А4 для факсимильных аппаратов', region: 'Новосибирская область', unit: 'упак', qty: 300, date: '2025-12-20' },
- { name: 'Бумага крафт упаковочная 84 см', region: 'Москва', unit: 'м', qty: 1500, date: '2025-12-15' },
-];
-
-let allResults = [], filteredResults = [], activeDateFilter = 90;
+let allResults = [], filteredResults = [];
const searchForm = document.getElementById('searchForm');
const searchBtn = document.getElementById('searchBtn');
@@ -767,13 +694,35 @@ document.getElementById('s_advToggle').addEventListener('click', function() {
});
// Submit search
-searchForm.addEventListener('submit', (e) => {
+searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
- const count = parseInt(document.getElementById('s_count').value) || 20;
- allResults = DEMO_DATA.slice(0, Math.min(count, DEMO_DATA.length));
+ const queryText = s_product.value.trim();
+ const topK = 20;
- const regions = [...new Set(allResults.map(r => r.region))];
- const units = [...new Set(allResults.map(r => r.unit))];
+ searchBtn.disabled = true;
+ searchBtn.textContent = 'Поиск…';
+
+ try {
+ const resp = await fetch('/app/api/search', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query_text: queryText, top_k: topK, use_rerank: false, rerank_strategy: 'lexical' })
+ });
+ if (!resp.ok) throw new Error('Ошибка сервера: ' + resp.status);
+ const data = await resp.json();
+ allResults = data.results || [];
+ } catch (err) {
+ alert('Не удалось выполнить поиск: ' + err.message);
+ searchBtn.disabled = false;
+ searchBtn.innerHTML = '
Найти похожие товары/услуги';
+ return;
+ }
+
+ searchBtn.disabled = false;
+ searchBtn.innerHTML = '
Найти похожие товары/услуги';
+
+ const regions = [...new Set(allResults.map(r => r.region).filter(Boolean))];
+ const units = [...new Set(allResults.map(r => r.unit).filter(Boolean))];
document.getElementById('f_region').innerHTML = '
' + regions.map(r => `
`).join('');
document.getElementById('f_unit').innerHTML = '
' + units.map(u => `
`).join('');
@@ -784,26 +733,13 @@ searchForm.addEventListener('submit', (e) => {
requestAnimationFrame(() => { document.getElementById('s_results').style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
});
-// Date chips
-document.querySelectorAll('.chip[data-days]').forEach(chip => {
- chip.addEventListener('click', () => {
- document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
- chip.classList.add('active');
- activeDateFilter = parseInt(chip.dataset.days);
- });
-});
-
function applySearchFilters() {
const rVal = document.getElementById('f_region').value;
const uVal = document.getElementById('f_unit').value;
- const qMin = parseFloat(document.getElementById('f_qtyMin').value) || 0;
- const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - activeDateFilter);
filteredResults = allResults.filter(r => {
if (rVal && r.region !== rVal) return false;
if (uVal && r.unit !== uVal) return false;
- if (qMin && r.qty < qMin) return false;
- if (new Date(r.date) < cutoff) return false;
return true;
});
renderSearchTable();
@@ -813,10 +749,6 @@ document.getElementById('applyFilters').addEventListener('click', applySearchFil
document.getElementById('resetFilters').addEventListener('click', () => {
document.getElementById('f_region').value = '';
document.getElementById('f_unit').value = '';
- document.getElementById('f_qtyMin').value = '';
- activeDateFilter = 90;
- document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
- document.querySelector('.chip[data-days="90"]').classList.add('active');
applySearchFilters();
});
@@ -831,11 +763,11 @@ function renderSearchTable() {
tbody.innerHTML = filteredResults.map((r, i) => `
| ${i + 1} |
- ${r.name} |
- ${r.region} |
- ${r.unit} |
- ${r.qty.toLocaleString('ru-RU')} |
- ${new Date(r.date).toLocaleDateString('ru-RU', {day:'2-digit',month:'2-digit',year:'numeric'})} |
+ ${r.raw_name || ''} |
+ ${r.norm_name || ''} |
+ ${r.region || '—'} |
+ ${r.unit || ''} |
+ ${r.contract_id || ''} |
`).join('');
}
@@ -848,75 +780,13 @@ const c_src = document.getElementById('c_sourceUnit');
const c_tgt = document.getElementById('c_targetUnit');
function validateConvert() {
- convertBtn.disabled = !(c_product.value.trim() && c_src.value && c_tgt.value);
+ convertBtn.disabled = !(c_product.value.trim() && c_src.value.trim() && c_tgt.value.trim());
}
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
-[c_src, c_tgt].forEach(el => el.addEventListener('change', validateConvert));
-
-// Demo logs
-function generateLogs(product, src, tgt) {
- const now = new Date();
- const t = (offset) => {
- const d = new Date(now.getTime() - offset);
- return d.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit',second:'2-digit'}) + '.' + String(d.getMilliseconds()).padStart(3,'0');
- };
- return [
- { time: t(2400), level: 'info', msg: `Получен запрос на конвертацию: "${product}"` },
- { time: t(2100), level: 'info', msg: `Параметры: source_unit=${src}, target_unit=${tgt}` },
- { time: t(1800), level: 'info', msg: 'Отправка запроса в Elasticsearch...' },
- { time: t(1200), level: 'ok', msg: `Elasticsearch: найдено 847 записей за 0.124s` },
- { time: t(1000), level: 'info', msg: 'Фильтрация записей с совпадающими ЕИ...' },
- { time: t(800), level: 'info', msg: `Отобрано 312 записей с ЕИ "${src}" и 89 с ЕИ "${tgt}"` },
- { time: t(600), level: 'info', msg: 'Расчёт медианного коэффициента пересчёта...' },
- { time: t(400), level: 'warn', msg: '3 выброса исключены из расчёта (отклонение > 3σ)' },
- { time: t(200), level: 'ok', msg: `Коэффициент рассчитан: 1 ${src} = 0.45 ${tgt}` },
- { time: t(50), level: 'ok', msg: 'Запрос выполнен успешно (2.35s)' },
- ];
-}
convertForm.addEventListener('submit', (e) => {
e.preventDefault();
- const product = c_product.value.trim();
- const src = c_src.value;
- const tgt = c_tgt.value;
-
- // Show result
- document.getElementById('c_empty').classList.add('hidden');
- const resultEl = document.getElementById('c_result');
- resultEl.classList.remove('hidden');
- resultEl.style.animation = 'none';
- requestAnimationFrame(() => { resultEl.style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
-
- // Demo coefficient
- const coeff = 0.45;
- document.getElementById('c_coeffValue').textContent = coeff;
- document.getElementById('c_coeffLabel').innerHTML = `${src} → ${tgt}`;
- document.getElementById('c_formula').textContent = `1 ${src} = ${coeff} ${tgt} • 1 ${tgt} = ${(1/coeff).toFixed(4)} ${src}`;
-
- // Logs
- const logs = generateLogs(product, src, tgt);
- document.getElementById('logsBadge').textContent = logs.length;
- document.getElementById('logsBody').innerHTML = logs.map(l =>
- `
${l.time}${l.level.toUpperCase().padEnd(4)}${l.msg}
`
- ).join('');
-});
-
-// Logs toggle
-document.getElementById('logsToggle').addEventListener('click', function() {
- const panel = document.getElementById('logsPanel');
- const open = panel.dataset.open === 'true';
- panel.dataset.open = String(!open);
- this.setAttribute('aria-expanded', String(!open));
-});
-
-// Copy logs
-document.getElementById('copyLogs').addEventListener('click', () => {
- const text = [...document.querySelectorAll('#logsBody .log-line')].map(l => l.textContent).join('\n');
- navigator.clipboard.writeText(text).then(() => {
- const btn = document.getElementById('copyLogs');
- btn.textContent = 'Скопировано';
- setTimeout(() => btn.textContent = 'Копировать', 1500);
- });
+ // TODO: подключить API конвертера, когда будет готов endpoint
});
diff --git a/www/rc_openapi.json b/www/rc_openapi.json
new file mode 100644
index 0000000..c091589
--- /dev/null
+++ b/www/rc_openapi.json
@@ -0,0 +1,178 @@
+{
+ "openapi": "3.1.0",
+ "info": { "title": "Product Matcher Region UI", "version": "1.0.0" },
+ "servers": [{ "url": "/region_search" }],
+ "paths": {
+ "/health": {
+ "get": {
+ "summary": "Health",
+ "operationId": "health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": { "application/json": { "schema": {} } }
+ }
+ }
+ }
+ },
+ "/": {
+ "get": {
+ "summary": "Home",
+ "operationId": "home__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": { "text/html": { "schema": { "type": "string" } } }
+ }
+ }
+ }
+ },
+ "/search": {
+ "post": {
+ "summary": "Search Form",
+ "operationId": "search_form_search_post",
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_search_form_search_post"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": { "text/html": { "schema": { "type": "string" } } }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/search": {
+ "post": {
+ "summary": "Search Api",
+ "operationId": "search_api_api_search_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/SearchRegionRequest" }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": { "application/json": { "schema": {} } }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Body_search_form_search_post": {
+ "properties": {
+ "query_text": {
+ "type": "string",
+ "title": "Query Text",
+ "default": "песчано-гравийная смесь фракция 0-40"
+ },
+ "region": {
+ "type": "string",
+ "title": "Region",
+ "default": "москва"
+ },
+ "top_k": { "type": "integer", "title": "Top K", "default": 20 },
+ "use_rerank": {
+ "anyOf": [{ "type": "string" }, { "type": "null" }],
+ "title": "Use Rerank"
+ },
+ "rerank_strategy": {
+ "type": "string",
+ "title": "Rerank Strategy",
+ "default": "lexical"
+ }
+ },
+ "type": "object",
+ "title": "Body_search_form_search_post"
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": { "$ref": "#/components/schemas/ValidationError" },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "SearchRegionRequest": {
+ "properties": {
+ "query_text": {
+ "type": "string",
+ "title": "Query Text",
+ "default": "песчано-гравийная смесь фракция 0-40"
+ },
+ "region": {
+ "type": "string",
+ "title": "Region",
+ "default": "москва"
+ },
+ "top_k": {
+ "type": "integer",
+ "maximum": 1000.0,
+ "minimum": 1.0,
+ "title": "Top K",
+ "default": 20
+ },
+ "use_rerank": {
+ "type": "boolean",
+ "title": "Use Rerank",
+ "default": false
+ },
+ "rerank_strategy": {
+ "type": "string",
+ "title": "Rerank Strategy",
+ "default": "lexical"
+ }
+ },
+ "type": "object",
+ "title": "SearchRegionRequest"
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": { "type": "string", "title": "Message" },
+ "type": { "type": "string", "title": "Error Type" },
+ "input": { "title": "Input" },
+ "ctx": { "type": "object", "title": "Context" }
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError"
+ }
+ }
+ }
+}
diff --git a/www/uc_openapi.json b/www/uc_openapi.json
new file mode 100644
index 0000000..ae0b798
--- /dev/null
+++ b/www/uc_openapi.json
@@ -0,0 +1,178 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Конвертер ЕИ (simple_frontend)",
+ "description": "Обёртка над `unit_converter/pipeline.py`: приём параметров конвертации, запуск пайплайна, возврат логов. Интерфейс: статическая страница `/`.",
+ "version": "1.0.0"
+ },
+ "servers": [{ "url": "/uc" }],
+ "paths": {
+ "/api/units": {
+ "get": {
+ "tags": ["Справочники"],
+ "summary": "Подсказки по единицам измерения",
+ "operationId": "api_units_api_units_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/UnitsResponse" }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/convert": {
+ "post": {
+ "tags": ["Конвертация"],
+ "summary": "Запуск конвертации (pipeline.py)",
+ "operationId": "api_convert_api_convert_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/ConvertRequest" }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/ConvertResponse" }
+ }
+ }
+ },
+ "400": { "description": "Не заполнены обязательные поля" },
+ "500": {
+ "description": "Ошибка запуска pipeline (например нет config/скрипта)"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ConvertRequest": {
+ "properties": {
+ "product_name": {
+ "type": "string",
+ "title": "Product Name",
+ "description": "Наименование товара или услуги"
+ },
+ "source_unit": {
+ "type": "string",
+ "title": "Source Unit",
+ "description": "Исходная единица измерения"
+ },
+ "target_unit": {
+ "type": "string",
+ "title": "Target Unit",
+ "description": "Целевая единица измерения"
+ },
+ "supplier_name": {
+ "type": "string",
+ "title": "Supplier Name",
+ "description": "Поставщик (необязательно)",
+ "default": ""
+ },
+ "product_characteristics": {
+ "type": "string",
+ "title": "Product Characteristics",
+ "description": "Текст характеристик (необязательно)",
+ "default": ""
+ },
+ "quantity": {
+ "type": "number",
+ "minimum": 0.0,
+ "title": "Quantity",
+ "description": "Количество в исходных ЕИ",
+ "default": 1.0
+ }
+ },
+ "type": "object",
+ "required": ["product_name", "source_unit", "target_unit"],
+ "title": "ConvertRequest"
+ },
+ "ConvertResponse": {
+ "properties": {
+ "process_ok": {
+ "type": "boolean",
+ "title": "Process Ok",
+ "description": "Процесс pipeline завершился с кодом 0"
+ },
+ "conversion_success": {
+ "type": "boolean",
+ "title": "Conversion Success",
+ "description": "True, если в выводе нет «перевод невозможен»"
+ },
+ "message": {
+ "type": "string",
+ "title": "Message",
+ "description": "Краткий итог для UI"
+ },
+ "pipeline_output": {
+ "type": "string",
+ "title": "Pipeline Output",
+ "description": "Полный stdout+stderr pipeline.py (логи)",
+ "default": ""
+ }
+ },
+ "type": "object",
+ "required": ["process_ok", "conversion_success", "message"],
+ "title": "ConvertResponse"
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": { "$ref": "#/components/schemas/ValidationError" },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "UnitsResponse": {
+ "properties": {
+ "units": {
+ "items": { "type": "string" },
+ "type": "array",
+ "title": "Units",
+ "description": "Список наименований ЕИ из exist_units.csv"
+ }
+ },
+ "type": "object",
+ "required": ["units"],
+ "title": "UnitsResponse"
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": { "type": "string", "title": "Message" },
+ "type": { "type": "string", "title": "Error Type" },
+ "input": { "title": "Input" },
+ "ctx": { "type": "object", "title": "Context" }
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError"
+ }
+ }
+ }
+}