feat(tradein/ui): передавать город в геокодирование + честный признак неопределённости (#2576) #2584

Merged
lekss361 merged 3 commits from feat/tradein-ui-city-hint into main 2026-07-31 15:46:02 +00:00
Owner

Эпик #2576, этап B — фронтовая половина к #2580. ⚠️ Мержить после #2580 (бэкенд владеет контрактом city_hint / target_city_ambiguous).

Без этого PR фикс геокодера половинчатый: бэкенд перестал молча подставлять Екатеринбург, но интерфейс город не передавал вовсе, и признак неопределённости срабатывал бы почти всегда.

Выбор города — выпадающий список + автоопределение. Список переиспользует существующий HUD-комбобокс панели, по умолчанию «Екатеринбург» — то есть ровно та строка, которую геокодер раньше подставлял молча, поэтому для Екатеринбурга поведение не меняется и лишних действий не требуется. Автоопределение (detectCityInText, с границами слов, чтобы «Серов» не срабатывал внутри «ул. Серова») синхронизирует список, когда пользователь печатает или выбирает подсказку с другим городом.

Текст при неопределённости (под полем адреса, предупреждающий цвет, role="status", не блокирует):

«Город определён автоматически — результат может относиться к другому населённому пункту области. Если это не {город}, выберите верный город выше и повторите оценку.»

Файлы: новый lib/city-registry.ts (6 городов + DEFAULT_CITY + детектор, по образцу source-registry.ts), типы, useGeocodeSuggest(query, cityHint?, limit) с city_hint в URL и ключе кэша, ParamsPanel (селектор, проброс в payload, плашка), app/v2/page.tsx.

Контракт сверен по фактическому коммиту 4b1a2822 из #2580, а не по описанию. npm run codegen не запускался — бэкенд-PR не смержен, а этот файл типов ведётся вручную (по его же заголовку).

tsc --noEmit + lint + build чисто (/v2 — 51.6 kB). Legacy-роут / не трогался: next.config.ts редиректит его на /v2.

Проверить визуально на проде после мержа: длинная подпись «Каменск-Уральский» при ширине 176px и перекрытие выпадающего списка с панелью подсказок адреса.

Эпик #2576, этап B — фронтовая половина к #2580. ⚠️ **Мержить после #2580** (бэкенд владеет контрактом `city_hint` / `target_city_ambiguous`). Без этого PR фикс геокодера половинчатый: бэкенд перестал молча подставлять Екатеринбург, но интерфейс город не передавал вовсе, и признак неопределённости срабатывал бы почти всегда. **Выбор города — выпадающий список + автоопределение.** Список переиспользует существующий HUD-комбобокс панели, по умолчанию «Екатеринбург» — то есть ровно та строка, которую геокодер раньше подставлял молча, поэтому **для Екатеринбурга поведение не меняется и лишних действий не требуется**. Автоопределение (`detectCityInText`, с границами слов, чтобы «Серов» не срабатывал внутри «ул. Серова») синхронизирует список, когда пользователь печатает или выбирает подсказку с другим городом. **Текст при неопределённости** (под полем адреса, предупреждающий цвет, `role="status"`, не блокирует): > «Город определён автоматически — результат может относиться к другому населённому пункту области. Если это не {город}, выберите верный город выше и повторите оценку.» **Файлы:** новый `lib/city-registry.ts` (6 городов + `DEFAULT_CITY` + детектор, по образцу `source-registry.ts`), типы, `useGeocodeSuggest(query, cityHint?, limit)` с `city_hint` в URL и ключе кэша, `ParamsPanel` (селектор, проброс в payload, плашка), `app/v2/page.tsx`. Контракт сверен по фактическому коммиту `4b1a2822` из #2580, а не по описанию. `npm run codegen` не запускался — бэкенд-PR не смержен, а этот файл типов ведётся вручную (по его же заголовку). `tsc --noEmit` + lint + build чисто (`/v2` — 51.6 kB). Legacy-роут `/` не трогался: `next.config.ts` редиректит его на `/v2`. Проверить визуально на проде после мержа: длинная подпись «Каменск-Уральский» при ширине 176px и перекрытие выпадающего списка с панелью подсказок адреса.
lekss361 added 1 commit 2026-07-31 14:09:51 +00:00
feat(tradein/ui): передавать город в геокодирование + честный признак неопределённости (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m7s
2779ef5e79
Раньше интерфейс город вообще не передавал — backend (#2580) больше не
подставляет "Екатеринбург" молча, из-за чего житель Нижнего Тагила, вводя
«Ленина, 1», получал бы результат по одноимённой екатеринбургской улице.

- Новый справочник src/lib/city-registry.ts (растущий список городов области,
  сейчас: Екатеринбург, Нижний Тагил, Каменск-Уральский, Первоуральск,
  Верхняя Пышма, Серов) — DEFAULT_CITY = Екатеринбург, чтобы ЕКБ-сценарий не
  требовал никаких лишних действий.
- ParamsPanel: компактный дропдаун «Город» рядом с лейблом адреса (переиспользует
  существующий <Dd> HUD-комбобокс) + автоопределение города из набранного
  текста/выбранной подсказки (detectCityInText, word-boundary safe — не путает
  "Серов" с "ул. Серова" в ЕКБ). city_hint уходит в geocode/suggest и в
  POST /trade-in/estimate.
- useGeocodeSuggest(query, cityHint, limit) — city_hint в query-параметрах и в
  queryKey, чтобы смена города рефетчила подсказки.
- Честная подсказка в ParamsPanel, когда estimate.target_city_ambiguous===true:
  спокойный (не danger) текст «Город определён автоматически — результат может
  относиться к другому населённому пункту области. Если это не {city},
  выберите верный город выше и повторите оценку.» — не блокирует форму.
- types/trade-in.ts: TradeInEstimateInput.city_hint,
  AggregatedEstimate.target_city_ambiguous (зеркалит backend PR #2580, ещё не
  смёржен — codegen не запускался, поля добавлены вручную по контракту схемы).

tsc --noEmit / next lint / next build — чисто (только 2 pre-existing warning
в несвязанных файлах).
bot-backend added 1 commit 2026-07-31 15:16:27 +00:00
fix(tradein/ui): не слать city_hint по умолчанию — только явный выбор или город из адреса (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m15s
a8d29fa031
Deep-review R2 на #2580/#2576: предыдущий коммит слал city_hint="Екатеринбург"
даже когда дропдаун не тронут — бэкенд трактует ЛЮБОЙ city_hint как «пользователь
назвал город» (city_specified=True), так что target_city_ambiguous становился
false практически всегда, а необнаруженный житель Нижнего Тагила («Ленина, 1»
без явного упоминания города) молча резолвился бы в Екатеринбург — ровно баг,
который чинил backend, только переехавший из geocoder.py в city-registry.ts.

Вариант A (по рекомендации ревьюера): город реально известен (и поэтому
отправляется в city_hint) ТОЛЬКО когда пользователь явно выбрал его в
дропдауне ИЛИ detectCityInText нашёл совпадение в наборном тексте / выбранной
подсказке. Нетронутый дефолт → city_hint не уходит вовсе (ни в geocode/suggest,
ни в POST /estimate) — тогда backend честно возвращает target_city_ambiguous и
не форсит ЕКБ-bias без запроса.

- Новое состояние `cityConfirmed` (ParamsPanel.tsx) — гейт на отправку,
  раздельный от `city` (best-guess для отображения/текста плашки). true после
  explicit dropdown pick ИЛИ автодетекта из текста/подсказки; sticky —
  мелкая правка адреса без нового совпадения его не сбрасывает.
- До подтверждения дропдаун показывает `UNCONFIRMED_CITY_LABEL`
  ("Определить автоматически"), не статичное "Екатеринбург" — не выдаёт
  внутренний best-guess за подтверждённый пользователем выбор.
- useGeocodeSuggest получает city_hint только при cityConfirmed=true — для
  нетронутой формы автокомплит тоже больше не форсит ЕКБ-bias молча, а видит
  кандидатов из всей области (в т.ч. Нижний Тагил) — это и есть тот сценарий
  из заголовка эпика.
- ЕКБ happy path не усложнён: как только пользователь печатает город в адресе
  или (обычный путь) выбирает любую подсказку из автокомплита, detectCityInText
  почти всегда находит "Екатеринбург" в full_address (провайдер возвращает
  город как часть резолвленного адреса независимо от того, был ли отправлен
  hint) — дропдаун сам переключается на "Екатеринбург" и cityConfirmed
  становится true без отдельного клика. Требует лишнего действия только
  редкий путь "напечатал произвольный адрес без города и нажал Enter, не
  выбрав ни одной подсказки".
- city-registry.ts: явный комментарий-ссылка на бэкендовый гэзеттир
  `SVERDLOVSK_OBLAST_CITIES` (tradein-mvp/backend/app/services/geocoder.py) —
  parity-риск при добавлении нового города остаётся видимым с фронтовой
  стороны (backend/тесты не трогаю — другой PR, вне моего scope).

tsc --noEmit / next lint / next build — чисто (только 2 pre-existing warning в
несвязанных файлах, как и в предыдущем коммите).
bot-backend added 1 commit 2026-07-31 15:43:34 +00:00
fix(tradein/ui): не называть неподтверждённый город в плашке неопределённости (#2576)
All checks were successful
CI / changes (pull_request) Successful in 14s
CI Trade-In / changes (pull_request) Successful in 14s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m33s
49ee85aae2
Deep-review R3: плашка city_ambiguous всегда подставляла {city} — внутреннее
состояние с дефолтом "Екатеринбург" (initCityLabel), а не то, что реально
определил бэкенд (в ответе только булев target_city_ambiguous, угаданного
города там нет). Ровно в целевом сценарии фикса — нетронутая форма, «Ленина
1», cityConfirmed=false — текст утверждал «если это не Екатеринбург»
независимо от реального результата (там мог быть Нижний Тагил) — та же
нечестность, которую предыдущий коммит убирал из запроса, только в тексте.

Текст плашки теперь ветвится по cityConfirmed:
- cityConfirmed=true (город реально был подтверждён и отправлен) — прежний
  текст с конкретным {city} уместен, не меняю.
- cityConfirmed=false (это и есть путь, где cityAmbiguous обычно и
  срабатывает после предыдущего коммита) — нейтральная формулировка без
  упоминания конкретного города: «Если это неверно, выберите город выше и
  повторите оценку.»

tsc --noEmit / next lint / next build — чисто (те же 2 pre-existing warning в
несвязанных файлах).
lekss361 merged commit a7fbd348e0 into main 2026-07-31 15:46:02 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2584
No description provided.