@@ -5,89 +5,144 @@
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Оценка поисковой выдачи · Акылдаш< / title >
< style >
: root { color-scheme : light ; font : 16 px / 1.5 system-ui , sans-serif ; color : #17202a ; background : #f5f7f a ; }
: root { color-scheme : light ; font : 16 px / 1.5 system-ui , sans-serif ; color : #17202a ; background : #eef2f5 ; --ink : #17202a ; --muted : #5d6b78 ; --line : #dbe2e8 ; --surface : #fff ; --surface-muted : #f7f9fb ; --brand : #0f5b78 ; --brand-dark : #0a4258 ; --accent : #e7b84b ; --focus : #1769a a ; }
* { box-sizing : border-box ; }
body { margin : 0 ; }
. review__skip { position : absolute ; inset-block-start : 0 ; inset-inline-start : -10000 px ; padding : .5 rem 1 rem ; background : #fff ; }
body { margin : 0 ; color : var ( - - ink ) ; background : radial-gradient ( circle at 85 % -10 % , #dcecf0 0 , transparent 35 rem ) , #eef2f5 ; }
. review__skip { position : absolute ; inset-block-start : 0 ; inset-inline-start : -10000 px ; padding : .5 rem 1 rem ; background : var ( - - surface ) ; }
. review__skip : focus { inset-inline-start : 1 rem ; z-index : 2 ; }
. review__header , . review__main { max-width : 144 0 px ; margin : auto ; padding-inline : 1 rem ; }
. review__header { padding-block : 1.5rem 1 rem ; }
. review__main { padding-block-end : 8 r em ; }
. review__s earch { display : flex ; flex-wrap : wrap ; align-items : end ; gap : .75 rem ; padd ing : 1 rem ; background : #fff ; border : 1 px solid #d9e0e7 ; border-radius : .7 5rem ; }
. review__field { display : grid ; gap : .25 rem ; min-width : 12 rem ; flex : 1 ; }
. review__field--small { flex : 0 0 7 rem ; min-width : 7 rem ; }
. review__header , . review__main { max-width : 132 0 px ; margin : auto ; padding-inline : clamp ( 1 rem , 3 vw , 3 rem ) ; }
. review__header { padding-block : clamp ( 2 rem , 5 vw , 4 rem ) 1.5 rem ; }
. review__header h1 { max-width : 18 ch ; margin : .35 rem 0 .75 rem ; font-size : clamp ( 2 rem , 4 vw , 3.5 rem ) ; line-height : 1.05 ; letter-spacing : -.04 em ; }
. review__h eader p { max-width : 58 ch ; marg in: 0 ; color : var ( - - muted ) ; font-size : 1.0 5rem ; }
. review__header-top { display : flex ; justify-content : space-between ; gap : 1 rem ; align-items : start ; }
. review__eyebrow { display : inline-flex ; align-items : center ; gap : .5 rem ; color : var ( - - brand ) ; font-size : .75 rem ; font-weight : 800 ; letter-spacing : .12 em ; text-transform : uppercase ; }
. review__eyebrow :: before { content : '' ; width : .55 rem ; height : .55 rem ; border-radius : 50 % ; background : var ( - - accent ) ; box-shadow : 0 0 0 4 px rgb ( 231 184 75 / .2 ) ; }
. review__main { padding-block-end : 6 rem ; }
. review__search { display : grid ; grid-template-columns : minmax ( 18 rem , 1 fr ) 9 rem 9 rem auto ; align-items : end ; gap : .75 rem ; padding : 1.25 rem ; background : var ( - - surface ) ; border : 1 px solid var ( - - line ) ; border-radius : 1 rem ; box-shadow : 0 1.25 rem 3 rem rgb ( 24 42 54 / .08 ) ; }
. review__field { display : grid ; gap : .35 rem ; min-width : 0 ; }
. review__field span , . review__actions label { color : var ( - - muted ) ; font-size : .8 rem ; font-weight : 700 ; }
. review__field--small { min-width : 0 ; }
input , select , textarea , button { font : inherit ; }
input , select , textarea { border : 1 px solid #8d9aaa ; border-radius : .4 rem ; padding : .6 rem .7 rem ; background : #fff ; }
input : focus-visible , select : focus-visible , textarea : focus-visible , button : focus-visible { outline : 3 px solid #1769aa ; outline-offset : 2 px ; }
button { cursor : pointer ; border : 1 px solid #536273 ; border-radius : .4 rem ; padd ing : .6 rem .9 rem ; background : #fff ; color : #17202a ; }
button : hover { background : #edf3f8 ; }
. review__button--primary { background : #145a86 ; color : #fff ; border-color : #145a86 ; }
. review__button--primary : ho ver { background : #0e4669 ; }
. review__status { min-height : 1.7 rem ; margin-block : .75 rem ; }
input , select , textarea { width : 100 % ; border: 1 px solid #b8c4ce ; border-radius : .6 rem ; padding : .7 rem .8 rem ; background : var ( - - surface ) ; color : var ( - - ink ) ; }
input :: placeholder , textarea :: placeholder { color : #87939e ; }
input : focus-visible , select : focus-visible , textarea : focus-visible , button : focus-visible { outl ine : 3 px solid var ( - - focus ) ; outline-offset : 2 px ; }
button { cursor : pointer ; border : 1 px solid #b8c4ce ; border-radius : .6 rem ; padding : .7 rem 1 rem ; background : var ( - - surface ) ; color : var ( - - ink ) ; font-weight : 700 ; transition-property : background-color , border-color , color , scale ; transition-duration : 150 ms ; transition-timing-function : cubic-bezier ( .2 , 0 , 0 , 1 ) ; }
button : hover { background : #edf4f7 ; border-color : #7d9aaa ; }
button : acti ve { scale : .96 ; }
button : disabled { cursor : wait ; opacity : .55 ; }
. review__button--primary { background : var ( - - brand ) ; color : #fff ; border-color : var ( - - brand ) ; }
. review__button--primary : hover { background : var ( - - brand - dark ) ; border-color : var ( - - brand - dark ) ; }
. review__status { min-height : 1.7 rem ; margin-block : 1 rem ; color : var ( - - muted ) ; }
. review__status--error { color : #9b1c1c ; }
. review__workspace { display : grid ; grid-template-columns : minmax ( 22 rem , 1 fr ) minmax ( 24 rem , 1.1 fr ) ; gap : 1 rem ; align-items : start ; }
. review__results , . review__document { background : #fff ; border : 1 px solid #d9e0e7 ; border-radius : .75 rem ; padding : 1 rem ; }
. review__results-list { display : grid ; gap : 1 rem ; margin : 0 ; padding : 0 ; list-style : none ; }
. review__result { border-block-start : 1 px solid #d9e0e7 ; padding-block-start : 1r em; }
. review__result : first-child { border-block-start : 0 ; padding-block-start : 0 ; }
. review__result-title { display : flex ; gap : .5 rem ; align-items : baseline ; width : 100 % ; text-align : start ; font-weight : 700 ; border : 0 ; padding : 0 ; color : #124f78 ; }
. review__rank { flex : 0 0 auto ; color : #526272 ; font-variant-numeric : tabular-nums ; }
. review__meta , . review__snippet { margin-block : .3 5 rem ; color : #526272 ; }
. review__workspace { display : grid ; grid-template-columns : minmax ( 24 rem , .9 fr ) minmax ( 28 rem , 1.1 fr ) ; gap : 1.25 rem ; align-items : start ; }
. review__results , . review__document { min-width : 0 ; background : var ( - - surface ) ; border : 1 px solid var ( - - line ) ; border-radius : 1 rem ; padding : clamp ( 1 rem , 2 vw , 1.5 rem ) ; box-shadow : 0 .75 rem 2 rem rgb ( 24 42 54 / .05 ) ; }
. review__panel-heading { display : flex ; justify-content : space-between ; gap : 1 rem ; align-items : baseline ; margin-block-end : 1 rem ; }
. review__panel-heading h2 { margin : 0 ; font-size : 1.1 rem ; letter-spacing : -.0 1em ; }
. review__panel-hint { color : var ( - - muted ) ; font-size : .8 rem ; }
. review__results-list { display : grid ; gap : .7 5 rem ; margin : 0 ; padding : 0 ; list-style : none ; }
. review__result { padding : 1 rem ; border : 1 px solid var ( - - line ) ; border-radius : .75 rem ; background : var ( - - surface - muted ) ; transition-property : background-color , border-color , box-shadow , scale ; transition-duration : 150 ms ; transition-timing-function : cubic-bezier ( .2 , 0 , 0 , 1 ) ; }
. review__result : hover , . review__result--selected { border-color : #8eb3c1 ; background : #f1f8fa ; box-shadow : 0 .5 rem 1 rem rgb ( 15 91 120 / .08 ) ; }
. review__result : first-child { border-block-start : 1 px solid var ( - - line ) ; }
. review__result-title { display : flex ; gap : .65 rem ; align-items : baseline ; width : 100 % ; text-align : start ; font-weight : 800 ; border : 0 ; padding : 0 ; background : none ; color : var ( - - brand - dark ) ; }
. review__result-title : hover { background : none ; border-color : transparent ; color : var ( - - brand ) ; }
. review__rank { flex : 0 0 auto ; color : var ( - - brand ) ; font-variant-numeric : tabular-nums ; }
. review__meta , . review__snippet { margin-block : .5 rem ; color : var ( - - muted ) ; }
. review__meta { font-size : .8 rem ; }
. review__snippet { overflow-wrap : anywhere ; }
. review__rating { display : flex ; flex-wrap : wrap ; gap : .45 rem ; margin-block : .6 5 rem ; padding : 0 ; border : 0 ; }
. review__rating legend { width : 100 % ; font-weight : 6 00; }
. review__rating label { min-width : 3.5 rem ; text-align : ce nter; }
. review__rating input { accent- color: #145a86 ; }
. review__comment { width : 100 % ; min-height : 4 rem ; resize : vertical ; }
. review__document { position : sticky ; inset-block-start : 1 rem ; min-heigh t : 20 rem ; }
. review__rating { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : .4 rem ; margin-block : 1 rem .7 5 rem ; padding : 0 ; border : 0 ; }
. review__rating legend { width : 100 % ; margin-block-end : .35 rem ; font-size : .8 rem ; font-weight : 7 00; color : var ( - - muted ) ; grid-column : 1 / -1 ; }
. review__rating label { min-width : 0 ; padding : .45 rem .25 rem ; border : 1 px solid #c5d0d8 ; border-radius : .5 rem ; color : var ( - - muted ) ; text-align : center ; font-size : .8 rem ; cursor : poi nter; }
. review__rating label : has ( input : checked ) { border-color : var ( - - brand ) ; background : var ( - - brand ) ; color: #fff ; }
. review__rating label : hover { border-color : #7d9aaa ; }
. review__rating label : has ( input : focus-visible ) { outline : 3 px solid var ( - - focus ) ; outline-offse t : 2 px ; }
. review__rating input { position : absolute ; opacity : 0 ; inline-size : 1 px ; block-size : 1 px ; }
. review__comment { min-height : 4 rem ; resize : vertical ; background : var ( - - surface ) ; }
. review__document { position : sticky ; inset-block-start : 1 rem ; min-height : 24 rem ; }
. review__document-meta { margin-block-end : 1 rem ; padding-block-end : 1 rem ; border-block-end : 1 px solid var ( - - line ) ; color : var ( - - muted ) ; }
. review__document-body { max-width : 75 ch ; overflow-wrap : anywhere ; }
. review__document-body img { max-width : 100 % ; height : auto ; }
. review__actions { position : fixed ; inset-block-end : 0 ; inset-inline : 0 ; padding : .75 rem 1 rem ; background : rgb ( 255 255 255 / .96 ) ; border-block-start : 1 px solid #d9e0e7 ; text-align : end ; }
dialog { max-width : min ( 56 rem , calc ( 100 % - 2 rem ) ) ; max-height : calc ( 100 % - 2 rem ) ; border : 1 px solid #8d9aaa ; border-radius : .75 rem ; padding : 1rem ; }
. review__actions { position : fixed ; inset-block-end : 0 ; inset-inline : 0 ; display : flex ; justify-content : center ; gap : .75 rem ; padding : .75 rem max ( 1 rem , env ( safe - area - inset - inline - start ) ) max ( .75 rem , env ( safe - area - inset - bottom ) ) ; background : rgb ( 255 255 255 / .94 ) ; border-block-start : 1 px solid var ( - - line ) ; box-shadow : 0 -.75 rem 2 rem rgb ( 24 42 54 / .06 ) ; backdrop-filter : blur ( 12 px ) ; }
. review__actions-inner { display : flex ; align-items : end ; gap : .75 rem ; width : min ( 1320 px , 100 % ) ; }
. review__actions label { flex : 1 ; }
. review__actions label : last-of-type { flex : 2 ; }
dialog { max-width : min ( 56 rem , calc ( 100 % - 2 rem ) ) ; max-height : calc ( 100 % - 2 rem ) ; border : 1 px solid var ( - - line ) ; border-radius : 1 rem ; padding : 1.5 rem ; box-shadow : 0 2 rem 5 rem rgb ( 10 20 30 / .2 ) ; }
dialog :: backdrop { background : rgb ( 10 20 30 / .5 ) ; }
. review__dialog-close { float : inline - end ; }
. review__empty { padding : 3 rem 1 rem ; color : var ( - - muted ) ; text-align : center ; }
. review__empty strong { display : block ; margin-block-end : .35 rem ; color : var ( - - ink ) ; }
. review__tour-spotlight { position : fixed ; z-index : 1 ; pointer-events : none ; border : 4 px solid var ( - - accent ) ; border-radius : .75 rem ; box-shadow : 0 0 0 100 vmax rgb ( 10 30 40 / .58 ) , 0 0 0 8 px rgb ( 231 184 75 / .25 ) ; }
. review__tour { width : min ( 34 rem , calc ( 100 % - 2 rem ) ) ; padding : 0 ; color : var ( - - ink ) ; }
. review__tour :: backdrop { background : transparent ; }
. review__tour-content { padding : clamp ( 1.25 rem , 4 vw , 2 rem ) ; }
. review__tour-kicker { margin : 0 0 .5 rem ; color : var ( - - brand ) ; font-size : .75 rem ; font-weight : 800 ; letter-spacing : .1 em ; text-transform : uppercase ; }
. review__tour h2 { margin : 0 0 .65 rem ; font-size : clamp ( 1.35 rem , 3 vw , 1.75 rem ) ; line-height : 1.1 ; letter-spacing : -.02 em ; }
. review__tour p { margin : 0 ; color : var ( - - muted ) ; }
. review__tour-footer { display : flex ; justify-content : space-between ; gap : .75 rem ; align-items : center ; margin-block-start : 1.5 rem ; }
. review__tour-progress { color : var ( - - muted ) ; font-size : .8 rem ; }
. review__tour-actions { display : flex ; gap : .5 rem ; }
@ media ( max-width : 800px ) {
. review__search { grid-template-columns : 1 fr 1 fr ; }
. review__search . review__field : first-child { grid-column : 1 / -1 ; }
. review__workspace { grid-template-columns : 1 fr ; }
. review__main { padding-block-end : 14 rem ; }
. review__document { display : none ; }
. review__search { align-items : stretch ; }
. review__field , . review__field--small { flex-basis : 100 % ; }
. review__search butt on { width : 100 % ; }
. review__document--tour-visible { display : block ; }
. review__search button { grid-column : 1 / -1 ; }
. review__acti ons { padding-inline : 1rem ; }
. review__actions-inner { flex-wrap : wrap ; }
. review__actions label { flex : 1 1 14 rem !important ; }
. review__actions button { flex : 1 1 100 % ; }
}
@ media ( max-width : 480px ) { . review__search { grid-template-columns : 1 fr ; } . review__search . review__field : first-child , . review__search button { grid-column : auto ; } . review__rating label { font-size : .72 rem ; } }
@ media ( prefers-reduced-motion : reduce ) { * , * :: before , * :: after { scroll-behavior : auto !important ; transition : none !important ; } }
< / style >
< / head >
< body >
< a class = "review__skip" href = "#results" > Перейти к результатам< / a >
< header class = "review__header" > < h1 > Оценка поисковой выдачи< / h1 > < p > Проверяйте результаты нашего OpenSearch по практическим юридическим запросам.< / p > < / header >
< header class = "review__header" >
< div class = "review__header-top" > < div class = "review__eyebrow" > Внутренняя лаборатория< / div > < button id = "tutorial-open" type = "button" > Как это работает< / button > < / div >
< h1 > Проверка качества поиска< / h1 > < p > Помогите проверить, насколько первые результаты отвечают на юридический запрос.< / p >
< / header >
< main class = "review__main" id = "review" >
< form class = "review__search" id = "search-form" >
< label class = "review__field" > З апрос< input id = "query" name = "q" maxlength = "500" required autocomplete = "off" > < / label >
< label class = "review__field review__field--small" > Язык< select id = "language" name = "language" > < option value = "ru" > Русский< / option > < option value = "ky" > Кыргызский< / option > < / select > < / label >
< label class = "review__field" > Юридический з апрос< input id = "query" name = "q" maxlength = "500" required autocomplete = "off" placeholder = "Например, как открыть О с О О ?" > < / label >
< label class = "review__field review__field--small" > Язык запроса < select id = "language" name = "language" > < option value = "ru" > Русский< / option > < option value = "ky" > Кыргызский< / option > < / select > < / label >
< label class = "review__field review__field--small" > Результатов< select id = "page-size" name = "page_size" > < option > 10< / option > < option > 20< / option > < / select > < / label >
< button class = "review__button--primary" type = "submit" > Найти < / button >
< button class = "review__button--primary" type = "submit" > Показать результаты < / button >
< / form >
< div class = "review__status" id = "status" role = "status" aria-live = "polite" > < / div >
< div class = "review__workspace" >
< section class = "review__results" aria-labelledby = "results-heading" > < h2 id = "results-heading" > Результаты< / h2 > < ol class = "review__results-list" id = "results" > < / ol > < / section >
< section class = "review__document" aria-labelledby = "document-heading" > < h2 id = "document-heading" > Д окумент< / h2 > < div id = "document-meta" > Выберите результат, чтобы открыть текс т.< / div > < article class = "review__document-body" id = "document-body" > < / article > < / section >
< section class = "review__results" aria-labelledby = "results-heading" > < div class = "review__panel-heading" > < h2 id = "results-heading" > Результаты поиска < / h2 > < span class = "review__panel-hint" > Оцените каждый документ< / span > < / div > < ol class = "review__results-list" id = "results" > < li class = "review__empty" > < strong > Начните с запроса< / strong > Введите юридический вопрос выше, чтобы увидеть результаты.< / li > < / ol > < / section >
< section class = "review__document" id = "document-panel" aria-labelledby = "document-heading" > < div class = "review__panel-heading" > < h2 id = "document-heading" > Просмотр д окумента < / h2 > < span class = "review__panel-hint" > Откройте результат< / span > < / div > < div class = "review__document-meta" id = "document-meta" > Выберите название в списке, чтобы прочитать докумен т.< / div > < article class = "review__document-body" id = "document-body" > < / article > < / section >
< / div >
< / main >
< div class = "review__actions" > < label > Проверяющий < input id = "reviewer" maxlength = "120" autocomplete = "name" > < / label > < label > Общий к омментарий < input id = "overall-comment" maxlength = "4000" > < / label > < button class = "review__button--primary" id = "save" type = "button" > Сохранить оцен ку< / button > < / div >
< div class = "review__actions" > < div class = "review__actions-inner" > < label > Ваше имя или псевдоним< input id = "reviewer" maxlength = "120" autocomplete = "name" placeholder = "Например, Айжан К ." > < / label > < label > К омментарий к выдаче < input id = "overall-comment" maxlength = "4000" placeholder = "Что важно учесть? (необязательно)" > < / label > < button class = "review__button--primary" id = "save" type = "button" > Сохранить размет ку< / button > < / div > < / div >
< dialog id = "document-dialog" > < button class = "review__dialog-close" id = "dialog-close" type = "button" > Закрыть< / button > < h2 id = "dialog-heading" > Документ< / h2 > < div class = "review__document-body" id = "dialog-body" > < / div > < / dialog >
< div class = "review__tour-spotlight" id = "tutorial-spotlight" hidden aria-hidden = "true" > < / div > < dialog class = "review__tour" id = "tutorial-dialog" aria-labelledby = "tutorial-heading" aria-describedby = "tutorial-text" > < div class = "review__tour-content" > < p class = "review__tour-kicker" > Короткое знакомство< / p > < h2 id = "tutorial-heading" > < / h2 > < p id = "tutorial-text" > < / p > < div class = "review__tour-footer" > < span class = "review__tour-progress" id = "tutorial-progress" > < / span > < div class = "review__tour-actions" > < button id = "tutorial-skip" type = "button" > Пропустить< / button > < button class = "review__button--primary" id = "tutorial-next" type = "button" > Далее< / button > < / div > < / div > < / div > < / dialog >
< div class = "review__status review__status--error" id = "error" role = "alert" aria-live = "assertive" > < / div >
< footer class = "review__header" > Акылдаш · Backend v0.9.0 · Внутренняя лаборатория релевантности< / footer >
< footer class = "review__header" > Акылдаш · Backend v0.9.2 · Внутренняя лаборатория релевантности< / footer >
< script >
const DRAFT _KEY = 'akyldash-search-review-draft' ;
const TOUR _KEY = 'akyldash-search-review-tour-seen' ;
const TOUR _STEPS = [
{ target : 'query' , title : 'Начните с юридического вопроса' , text : 'Введите практический запрос, например «как открыть О с О О ?», выберите язык и нажмите «Показать результаты».' , label : 'Шаг 1 из 4' } ,
{ target : 'results' , title : 'Смотрите порядок выдачи' , text : 'Документы расположены в том порядке, в котором их нашёл поиск. Номер слева — позиция результата.' , label : 'Шаг 2 из 4' } ,
{ target : 'document-panel' , title : 'Читайте документ перед оценкой' , text : 'Нажмите на название результата. Справа откроется текст документа и ссылка на официальный источник.' , label : 'Шаг 3 из 4' } ,
{ target : 'save' , title : 'Сохраните свою разметку' , text : 'Поставьте оценку каждому просмотренному документу, добавьте комментарии и сохраните результат внизу страницы.' , label : 'Шаг 4 из 4' }
] ;
const state = { results : [ ] , token : '' , selected : null , query : '' } ;
const $ = ( id ) => document . getElementById ( id ) ;
const setStatus = ( text ) => { $ ( 'status' ) . textContent = text ; $ ( 'error' ) . textContent = '' ; } ;
const setError = ( text ) => { $ ( 'error' ) . textContent = text ; } ;
const escapeText = ( value ) => value == null ? '' : String ( value ) ;
const readDraft = ( ) => { try { return JSON . parse ( localStorage . getItem ( DRAFT _KEY ) || 'null' ) ; } catch ( _ ) { return null ; } } ;
const hasSeenTour = ( ) => { try { return localStorage . getItem ( TOUR _KEY ) === '1' ; } catch ( _ ) { return false ; } } ;
const markTourSeen = ( ) => { try { localStorage . setItem ( TOUR _KEY , '1' ) ; } catch ( _ ) { } } ;
const saveDraft = ( ) => { if ( ! state . token ) return ; const results = [ ... $ ( 'results' ) . children ] . map ( ( item , index ) => ( { rank : index + 1 , code : item . dataset . code , rating : item . querySelector ( 'input:checked' ) ? . value ? ? null , comment : item . querySelector ( 'textarea' ) . value } ) ) ; try { localStorage . setItem ( DRAFT _KEY , JSON . stringify ( { query : state . query , language : $ ( 'language' ) . value , pageSize : $ ( 'page-size' ) . value , reviewer : $ ( 'reviewer' ) . value , overallComment : $ ( 'overall-comment' ) . value , results } ) ) ; } catch ( _ ) { } } ;
const applyDraft = ( ) => { const draft = readDraft ( ) ; if ( ! draft || draft . query !== state . query || draft . language !== $ ( 'language' ) . value || draft . pageSize !== $ ( 'page-size' ) . value ) return ; $ ( 'reviewer' ) . value = draft . reviewer || '' ; $ ( 'overall-comment' ) . value = draft . overallComment || '' ; ( draft . results || [ ] ) . forEach ( ( saved ) => { const item = [ ... $ ( 'results' ) . children ] . find ( ( candidate ) => candidate . dataset . rank === String ( saved . rank ) && candidate . dataset . code === saved . code ) ; if ( ! item ) return ; if ( saved . rating != null ) { const input = item . querySelector ( ` input[value=" ${ CSS . escape ( String ( saved . rating ) ) } "] ` ) ; if ( input ) input . checked = true ; } item . querySelector ( 'textarea' ) . value = saved . comment || '' ; } ) ; } ;
function renderResults ( ) {
$ ( 'results' ) . replaceChildren ( ) ;
if ( ! state . results . length ) { $ ( 'results' ) . innerHTML = '<li class="review__empty"><strong>Результатов пока нет</strong>Введите другой запрос и попробуйте снова.</li>' ; return ; }
state . results . forEach ( ( result , index ) => {
const item = document . createElement ( 'li' ) ; item . className = 'review__result' ; item . dataset . rank = index + 1 ; item . dataset . code = result . code ;
const title = document . createElement ( 'button' ) ; title . type = 'button' ; title . className = 'review__result-title' ; title . innerHTML = ` <span class="review__rank"># ${ index + 1 } </span><span></span> ` ; title . lastElementChild . textContent = escapeText ( result . name || 'Название не указано' ) ; title . addEventListener ( 'click' , ( ) => openDocument ( index , title ) ) ;
@@ -101,14 +156,21 @@
applyDraft ( ) ;
}
async function openDocument ( index , trigger ) {
const result = state . results [ index ] ; state . selected = trigger ; setStatus ( 'Загрузка документа…' ) ;
const result = state . results [ index ] ; state . selected = trigger ; [ ... $ ( 'results' ) . children ] . forEach ( ( item ) => item . classList . remove ( 'review__result--selected' ) ) ; trigger . closest ( '.review__result' ) ? . classList . add ( 'review__result--selected' ) ; setStatus ( 'Загрузка документа…' ) ;
try { const response = await fetch ( ` /documents/ ${ encodeURIComponent ( result . code ) } /editions/ ${ encodeURIComponent ( result . edition ) } ?language= ${ encodeURIComponent ( $ ( 'language' ) . value ) } ` ) ; if ( ! response . ok ) throw new Error ( 'Документ недоступен' ) ; const payload = await response . json ( ) ; const content = payload . content [ $ ( 'language' ) . value ] ; const meta = $ ( 'document-meta' ) ; meta . textContent = [ result . name , result . status , result . date _adopted ] . filter ( Boolean ) . join ( ' · ' ) ; const source = document . createElement ( 'a' ) ; source . href = ` https://cbd.minjust.gov.kg/ ${ encodeURIComponent ( result . code ) } /edition/ ${ encodeURIComponent ( result . edition ) } / ${ encodeURIComponent ( $ ( 'language' ) . value ) } ` ; source . target = '_blank' ; source . rel = 'noreferrer' ; source . textContent = ' Официальный источник' ; meta . append ( source ) ; $ ( 'document-body' ) . innerHTML = content . html ; $ ( 'dialog-heading' ) . textContent = result . name || 'Документ' ; $ ( 'dialog-body' ) . innerHTML = content . html ; if ( matchMedia ( '(max-width: 800px)' ) . matches ) $ ( 'document-dialog' ) . showModal ( ) ; setStatus ( 'Документ загружен.' ) ; } catch ( error ) { setError ( 'Н е удалось загрузить документ. Повторите попытку.' ) ; }
}
$ ( 'dialog-close' ) . addEventListener ( 'click' , ( ) => { $ ( 'document-dialog' ) . close ( ) ; if ( state . selected ) state . selected . focus ( ) ; } ) ;
let tutorialStep = 0 ; let tutorialReturnFocus = null ;
function clearTourTarget ( ) { $ ( 'tutorial-spotlight' ) . hidden = true ; $ ( 'document-panel' ) . classList . remove ( 'review__document--tour-visible' ) ; }
function renderTutorial ( ) { const step = TOUR _STEPS [ tutorialStep ] ; const target = $ ( step . target ) ; if ( step . target === 'document-panel' && getComputedStyle ( target ) . display === 'none' ) target . classList . add ( 'review__document--tour-visible' ) ; target . scrollIntoView ( { block : 'nearest' , inline : 'nearest' } ) ; const bounds = target . getBoundingClientRect ( ) ; $ ( 'tutorial-spotlight' ) . style . cssText = ` inset-block-start: ${ Math . max ( 8 , bounds . top - 4 ) } px; inset-inline-start: ${ Math . max ( 8 , bounds . left - 4 ) } px; inline-size: ${ bounds . width + 8 } px; block-size: ${ bounds . height + 8 } px; ` ; $ ( 'tutorial-spotlight' ) . hidden = false ; $ ( 'tutorial-heading' ) . textContent = step . title ; $ ( 'tutorial-text' ) . textContent = step . text ; $ ( 'tutorial-progress' ) . textContent = step . label ; $ ( 'tutorial-next' ) . textContent = tutorialStep === TOUR _STEPS . length - 1 ? 'Понятно' : 'Далее' ; }
function closeTutorial ( ) { clearTourTarget ( ) ; $ ( 'tutorial-dialog' ) . close ( ) ; markTourSeen ( ) ; tutorialReturnFocus ? . focus ( ) ; }
function openTutorial ( ) { tutorialReturnFocus = document . activeElement ; tutorialStep = 0 ; renderTutorial ( ) ; $ ( 'tutorial-dialog' ) . showModal ( ) ; $ ( 'tutorial-next' ) . focus ( ) ; }
$ ( 'tutorial-open' ) . addEventListener ( 'click' , openTutorial ) ; $ ( 'tutorial-next' ) . addEventListener ( 'click' , ( ) => { if ( tutorialStep === TOUR _STEPS . length - 1 ) closeTutorial ( ) ; else { tutorialStep += 1 ; renderTutorial ( ) ; } } ) ; $ ( 'tutorial-skip' ) . addEventListener ( 'click' , closeTutorial ) ; $ ( 'tutorial-dialog' ) . addEventListener ( 'cancel' , ( event ) => { event . preventDefault ( ) ; closeTutorial ( ) ; } ) ;
$ ( 'search-form' ) . addEventListener ( 'submit' , async ( event ) => { event . preventDefault ( ) ; const query = $ ( 'query' ) . value . trim ( ) ; if ( ! query ) return ; state . query = query ; setStatus ( 'Поиск выполняется…' ) ; $ ( 'save' ) . disabled = true ; try { const params = new URLSearchParams ( { q : query , language : $ ( 'language' ) . value , page _size : $ ( 'page-size' ) . value } ) ; const response = await fetch ( ` /search? ${ params } ` ) ; if ( ! response . ok ) throw new Error ( ) ; const payload = await response . json ( ) ; state . results = payload . results ; state . token = payload . review _token ; renderResults ( ) ; setStatus ( state . results . length ? ` Найдено результатов: ${ state . results . length } . ` : ` По запросу « ${ query } » ничего не найдено. Измените запрос. ` ) ; } catch ( error ) { state . results = [ ] ; state . token = '' ; renderResults ( ) ; setError ( 'Н е удалось выполнить поиск. Повторите поиск.' ) ; } finally { $ ( 'save' ) . disabled = false ; } } ) ;
document . addEventListener ( 'input' , saveDraft ) ; document . addEventListener ( 'change' , saveDraft ) ;
$ ( 'save' ) . addEventListener ( 'click' , async ( ) => { if ( ! state . token ) { setError ( 'Сначала выполните поиск.' ) ; return ; } const reviewer = $ ( 'reviewer' ) . value . trim ( ) ; if ( ! reviewer ) { $ ( 'reviewer' ) . focus ( ) ; setError ( 'Укажите проверяющего.' ) ; return ; } const results = [ ... $ ( 'results' ) . children ] . map ( ( item , index ) => ( { rank : index + 1 , code : item . dataset . code , rating : item . querySelector ( 'input:checked' ) ? Number ( item . querySelector ( 'input:checked' ) . value ) : null , comment : item . querySelector ( 'textarea' ) . value } ) ) ; $ ( 'save' ) . disabled = true ; setStatus ( 'Сохранение выполняется…' ) ; try { const response = await fetch ( '/search-reviews' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { review _token : state . token , reviewer , overall _comment : $ ( 'overall-comment' ) . value , results } ) } ) ; if ( ! response . ok ) throw new Error ( ) ; const payload = await response . json ( ) ; localStorage . removeItem ( DRAFT _KEY ) ; setStatus ( ` Оценка сохранена · № ${ payload . id } . ` ) ; } catch ( error ) { setError ( 'Н е удалось сохранить. Проверьте подключение и повторите.' ) ; } finally { $ ( 'save' ) . disabled = false ; } } ) ;
const draft = readDraft ( ) ; if ( draft ) { $ ( 'query' ) . value = draft . query || '' ; $ ( 'language' ) . value = draft . language || 'ru' ; $ ( 'page-size' ) . value = draft . pageSize || '20' ; $ ( 'reviewer' ) . value = draft . reviewer || '' ; $ ( 'overall-comment' ) . value = draft . overallComment || '' ; }
if ( ! hasSeenTour ( ) ) setTimeout ( openTutorial , 350 ) ;
< / script >
< / body >
< / html >