index.html 99 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>搜索评估 · 案例总览</title>
  7. <style>
  8. :root {
  9. --ink: #24211d;
  10. --muted: #6f6961;
  11. --line: #ded8ce;
  12. --paper: #fbfaf7;
  13. --panel: #ffffff;
  14. --mint: #1f8a70;
  15. --rose: #b24b63;
  16. --amber: #b87918;
  17. --cyan: #2a6f8f;
  18. --soft-mint: #e9f5f0;
  19. --soft-rose: #f8e9ee;
  20. --soft-amber: #fff2d9;
  21. --soft-cyan: #e7f2f7;
  22. --shadow: 0 18px 45px rgba(41, 35, 28, .08);
  23. }
  24. * {
  25. box-sizing: border-box;
  26. }
  27. body {
  28. margin: 0;
  29. color: var(--ink);
  30. background: var(--paper);
  31. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  32. line-height: 1.55;
  33. }
  34. header {
  35. padding: 40px 32px 22px;
  36. border-bottom: 1px solid var(--line);
  37. background: linear-gradient(180deg, #fff 0%, #fbfaf7 100%);
  38. }
  39. .wrap {
  40. max-width: 1220px;
  41. margin: 0 auto;
  42. }
  43. .eyebrow {
  44. display: flex;
  45. gap: 8px;
  46. align-items: center;
  47. color: var(--mint);
  48. font-size: 13px;
  49. font-weight: 700;
  50. text-transform: uppercase;
  51. letter-spacing: 0;
  52. }
  53. h1 {
  54. margin: 10px 0 10px;
  55. font-size: clamp(32px, 5vw, 58px);
  56. line-height: 1.06;
  57. letter-spacing: 0;
  58. }
  59. .lede {
  60. max-width: 860px;
  61. margin: 0;
  62. color: var(--muted);
  63. font-size: 17px;
  64. }
  65. .stats {
  66. display: grid;
  67. grid-template-columns: repeat(4, minmax(0, 1fr));
  68. gap: 12px;
  69. margin-top: 24px;
  70. }
  71. .stat {
  72. background: var(--panel);
  73. border: 1px solid var(--line);
  74. border-radius: 8px;
  75. padding: 14px;
  76. box-shadow: var(--shadow);
  77. min-height: 86px;
  78. }
  79. .stat strong {
  80. display: block;
  81. font-size: 26px;
  82. line-height: 1.1;
  83. }
  84. .stat span {
  85. color: var(--muted);
  86. font-size: 13px;
  87. }
  88. main {
  89. padding: 24px 32px 48px;
  90. }
  91. .toolbar {
  92. display: flex;
  93. gap: 10px;
  94. align-items: center;
  95. justify-content: space-between;
  96. margin-bottom: 18px;
  97. flex-wrap: wrap;
  98. }
  99. .filters {
  100. display: flex;
  101. gap: 8px;
  102. flex-wrap: wrap;
  103. }
  104. button,
  105. select {
  106. border: 1px solid var(--line);
  107. background: #fff;
  108. color: var(--ink);
  109. border-radius: 8px;
  110. padding: 9px 12px;
  111. font: inherit;
  112. }
  113. button {
  114. cursor: pointer;
  115. }
  116. button.active {
  117. color: #fff;
  118. background: var(--ink);
  119. border-color: var(--ink);
  120. }
  121. select {
  122. min-width: 190px;
  123. }
  124. .grid {
  125. display: grid;
  126. grid-template-columns: repeat(3, minmax(0, 1fr));
  127. gap: 16px;
  128. }
  129. .result {
  130. min-height: 500px;
  131. background: var(--panel);
  132. border: 1px solid var(--line);
  133. border-radius: 8px;
  134. overflow: hidden;
  135. box-shadow: var(--shadow);
  136. display: flex;
  137. flex-direction: column;
  138. position: relative;
  139. transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.25s ease;
  140. }
  141. .result:hover {
  142. transform: translateY(-2px);
  143. box-shadow: 0 22px 50px rgba(41, 35, 28, .12);
  144. }
  145. .result.discard {
  146. border-color: rgba(178, 75, 99, 0.15);
  147. }
  148. .discard-overlay {
  149. position: absolute;
  150. top: 0;
  151. left: 0;
  152. right: 0;
  153. bottom: 0;
  154. background: rgba(251, 250, 247, 0.85);
  155. backdrop-filter: blur(5px);
  156. -webkit-backdrop-filter: blur(5px);
  157. display: flex;
  158. flex-direction: column;
  159. align-items: center;
  160. justify-content: center;
  161. padding: 20px;
  162. text-align: center;
  163. z-index: 5;
  164. opacity: 1;
  165. transition: opacity 0.25s ease;
  166. pointer-events: none;
  167. }
  168. .result.discard:hover .discard-overlay {
  169. opacity: 0;
  170. }
  171. .discard-badge {
  172. background: var(--soft-rose);
  173. color: var(--rose);
  174. border: 1px solid rgba(178, 75, 99, 0.25);
  175. padding: 6px 16px;
  176. border-radius: 999px;
  177. font-size: 13px;
  178. font-weight: 700;
  179. text-transform: uppercase;
  180. letter-spacing: 1px;
  181. margin-bottom: 12px;
  182. box-shadow: 0 2px 8px rgba(178, 75, 99, 0.1);
  183. }
  184. .discard-reason {
  185. color: var(--muted);
  186. font-size: 13px;
  187. line-height: 1.6;
  188. max-width: 90%;
  189. margin: 0 auto;
  190. display: -webkit-box;
  191. -webkit-line-clamp: 6;
  192. -webkit-box-orient: vertical;
  193. overflow: hidden;
  194. font-weight: 500;
  195. }
  196. .thumbs {
  197. display: grid;
  198. grid-template-columns: repeat(3, 1fr);
  199. gap: 2px;
  200. height: 150px;
  201. background: #eee7dc;
  202. overflow: hidden;
  203. }
  204. .thumbs img {
  205. width: 100%;
  206. height: 100%;
  207. object-fit: cover;
  208. display: block;
  209. background: #eee7dc;
  210. }
  211. .thumbs img:first-child:nth-last-child(1) {
  212. grid-column: 1 / -1;
  213. }
  214. .body {
  215. padding: 15px;
  216. flex: 1;
  217. display: flex;
  218. flex-direction: column;
  219. }
  220. .meta {
  221. display: flex;
  222. justify-content: space-between;
  223. gap: 10px;
  224. color: var(--muted);
  225. font-size: 12px;
  226. margin-bottom: 8px;
  227. }
  228. .platform {
  229. color: #fff;
  230. border-radius: 999px;
  231. padding: 2px 8px;
  232. font-weight: 700;
  233. white-space: nowrap;
  234. }
  235. .p-xhs {
  236. background: var(--rose);
  237. }
  238. .p-gzh {
  239. background: var(--mint);
  240. }
  241. .p-x {
  242. background: var(--cyan);
  243. }
  244. h2 {
  245. margin: 0 0 8px;
  246. font-size: 18px;
  247. line-height: 1.25;
  248. letter-spacing: 0;
  249. }
  250. .excerpt {
  251. color: var(--muted);
  252. font-size: 13px;
  253. display: -webkit-box;
  254. -webkit-line-clamp: 5;
  255. -webkit-box-orient: vertical;
  256. overflow: hidden;
  257. }
  258. .tags {
  259. display: flex;
  260. flex-wrap: wrap;
  261. gap: 6px;
  262. margin: 12px 0;
  263. }
  264. .tag {
  265. background: #f2eee7;
  266. border-radius: 999px;
  267. padding: 3px 8px;
  268. font-size: 12px;
  269. color: #514a42;
  270. }
  271. .scorebar {
  272. margin-top: auto;
  273. }
  274. .overall {
  275. display: flex;
  276. align-items: end;
  277. justify-content: space-between;
  278. border-top: 1px solid var(--line);
  279. padding-top: 12px;
  280. }
  281. .score {
  282. font-size: 36px;
  283. line-height: .9;
  284. font-weight: 800;
  285. }
  286. .decision {
  287. color: var(--mint);
  288. font-weight: 800;
  289. }
  290. .decision.discard {
  291. color: var(--amber);
  292. }
  293. .mini-bars {
  294. display: grid;
  295. grid-template-columns: repeat(5, 1fr);
  296. gap: 4px;
  297. margin-top: 10px;
  298. }
  299. .mini-bars span {
  300. height: 7px;
  301. border-radius: 999px;
  302. background: #eee;
  303. overflow: hidden;
  304. position: relative;
  305. }
  306. .mini-bars span .fill {
  307. display: block;
  308. height: 100%;
  309. background: var(--mint);
  310. border-radius: 999px;
  311. }
  312. .group-snapshot {
  313. display: grid;
  314. grid-template-columns: repeat(4, minmax(0, 1fr));
  315. gap: 5px;
  316. margin-top: 10px;
  317. }
  318. .mini-bars.new-schema {
  319. grid-template-columns: repeat(2, 1fr) !important;
  320. }
  321. .group-snapshot.new-schema {
  322. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  323. }
  324. .group-pill {
  325. border: 1px solid var(--line);
  326. border-radius: 8px;
  327. padding: 5px 6px;
  328. background: #fff;
  329. min-width: 0;
  330. }
  331. .group-pill span {
  332. display: block;
  333. color: var(--muted);
  334. font-size: 11px;
  335. white-space: nowrap;
  336. overflow: hidden;
  337. text-overflow: ellipsis;
  338. }
  339. .group-pill strong {
  340. display: block;
  341. font-size: 14px;
  342. line-height: 1.1;
  343. }
  344. .actions {
  345. display: flex;
  346. gap: 8px;
  347. margin-top: 12px;
  348. }
  349. .actions a,
  350. .actions button {
  351. flex: 1;
  352. text-align: center;
  353. text-decoration: none;
  354. color: var(--ink);
  355. background: #fff;
  356. border: 1px solid var(--line);
  357. border-radius: 8px;
  358. padding: 8px 10px;
  359. font-size: 13px;
  360. }
  361. dialog {
  362. width: min(980px, calc(100vw - 28px));
  363. max-height: calc(100vh - 32px);
  364. border: 1px solid var(--line);
  365. border-radius: 8px;
  366. padding: 0;
  367. box-shadow: 0 28px 90px rgba(0, 0, 0, .25);
  368. }
  369. /* 全屏模式: 占满视口 + 改用 flex 列布局, 让内容面板/工序 iframe 撑满。
  370. 必须带 [open] —— 否则 display:flex 会盖过 UA 的 dialog:not([open]){display:none},
  371. 导致关闭后对话框还赖在屏上 (要再点一下才消失)。 */
  372. dialog.fullscreen[open] {
  373. width: 100vw;
  374. height: 100vh;
  375. max-width: none;
  376. max-height: none;
  377. border: none;
  378. border-radius: 0;
  379. display: flex;
  380. flex-direction: column;
  381. }
  382. dialog.fullscreen .modal-head,
  383. dialog.fullscreen .modal-tabs {
  384. flex: 0 0 auto; /* 头部/标签固定高, 不被压缩 */
  385. }
  386. dialog.fullscreen .modal-content,
  387. dialog.fullscreen #modalContentProcedure {
  388. flex: 1 1 auto; /* 只有内容面板撑满剩余高度 */
  389. min-height: 0;
  390. overflow: auto;
  391. }
  392. dialog.fullscreen #modalContentProcedure {
  393. height: auto !important; /* 覆盖内联的固定 600px, 改由 flex 撑满 */
  394. }
  395. dialog::backdrop {
  396. background: rgba(38, 33, 27, .42);
  397. }
  398. .modal-head {
  399. position: sticky;
  400. top: 0;
  401. background: #fff;
  402. border-bottom: 1px solid var(--line);
  403. padding: 16px;
  404. z-index: 2;
  405. display: flex;
  406. justify-content: space-between;
  407. gap: 14px;
  408. align-items: start;
  409. }
  410. .modal-head h3 {
  411. margin: 0;
  412. font-size: 20px;
  413. line-height: 1.25;
  414. }
  415. .modal-content {
  416. display: grid;
  417. grid-template-columns: 1.1fr .9fr;
  418. gap: 18px;
  419. padding: 16px;
  420. }
  421. .modal-content > section,
  422. .modal-content > aside {
  423. min-width: 0;
  424. }
  425. .section-title {
  426. margin: 18px 0 8px;
  427. font-weight: 800;
  428. }
  429. .raw {
  430. white-space: pre-wrap;
  431. background: #faf7f1;
  432. border: 1px solid var(--line);
  433. border-radius: 8px;
  434. padding: 12px;
  435. max-height: 330px;
  436. overflow: auto;
  437. color: #3d3831;
  438. font-size: 13px;
  439. }
  440. .images {
  441. display: grid;
  442. grid-template-columns: repeat(2, minmax(0, 1fr));
  443. gap: 8px;
  444. }
  445. .images img {
  446. width: 100%;
  447. max-height: 260px;
  448. object-fit: contain;
  449. border: 1px solid var(--line);
  450. border-radius: 8px;
  451. background: #f1ece4;
  452. }
  453. .scores {
  454. display: grid;
  455. gap: 12px;
  456. }
  457. .score-group {
  458. border: 1px solid var(--line);
  459. border-radius: 8px;
  460. background: #fff;
  461. overflow: hidden;
  462. }
  463. .score-group-head {
  464. display: flex;
  465. justify-content: space-between;
  466. gap: 10px;
  467. align-items: center;
  468. padding: 10px 11px;
  469. background: #faf7f1;
  470. border-bottom: 1px solid var(--line);
  471. font-size: 13px;
  472. font-weight: 800;
  473. }
  474. .score-group-head small {
  475. color: var(--muted);
  476. font-weight: 700;
  477. white-space: nowrap;
  478. }
  479. .score-group-body {
  480. display: grid;
  481. gap: 8px;
  482. padding: 10px;
  483. }
  484. .score-row {
  485. display: grid;
  486. grid-template-columns: 128px 1fr 34px;
  487. gap: 10px;
  488. align-items: center;
  489. font-size: 13px;
  490. }
  491. .score-row.missing {
  492. color: #a39b91;
  493. }
  494. .score-row.missing .meter span {
  495. display: none;
  496. }
  497. .meter {
  498. height: 9px;
  499. border-radius: 999px;
  500. background: #eee7dc;
  501. overflow: hidden;
  502. }
  503. .meter span {
  504. display: block;
  505. height: 100%;
  506. width: calc(var(--v) * 20%);
  507. background: var(--rose);
  508. }
  509. .rubric-note {
  510. background: var(--soft-cyan);
  511. border-left: 4px solid var(--cyan);
  512. padding: 10px 12px;
  513. color: #254c5d;
  514. border-radius: 4px;
  515. font-size: 13px;
  516. }
  517. @media (max-width: 980px) {
  518. .grid {
  519. grid-template-columns: repeat(2, minmax(0, 1fr));
  520. }
  521. .stats {
  522. grid-template-columns: repeat(2, minmax(0, 1fr));
  523. }
  524. .modal-content {
  525. grid-template-columns: 1fr;
  526. }
  527. }
  528. @media (max-width: 640px) {
  529. header,
  530. main {
  531. padding-left: 16px;
  532. padding-right: 16px;
  533. }
  534. .grid,
  535. .stats {
  536. grid-template-columns: 1fr;
  537. }
  538. .toolbar {
  539. align-items: stretch;
  540. }
  541. select {
  542. width: 100%;
  543. }
  544. .result {
  545. min-height: auto;
  546. }
  547. .group-snapshot {
  548. grid-template-columns: repeat(2, minmax(0, 1fr));
  549. }
  550. }
  551. /* Extra styles for interactive navigation & matrix */
  552. .stats {
  553. display: grid;
  554. grid-template-columns: repeat(5, 1fr);
  555. gap: 12px;
  556. }
  557. .stats .stat {
  558. min-width: 0;
  559. }
  560. .p-zhihu {
  561. background: #2a6f8f;
  562. }
  563. .p-x {
  564. background: #2a6f8f;
  565. }
  566. .p-bili {
  567. background: #b24b63;
  568. }
  569. .p-douyin {
  570. background: #24211d;
  571. }
  572. .p-sph {
  573. background: #07c160;
  574. }
  575. .p-youtube {
  576. background: #c4302b;
  577. }
  578. .p-github {
  579. background: #24292e;
  580. }
  581. .p-toutiao {
  582. background: #f04142;
  583. }
  584. .p-weibo {
  585. background: #e6162d;
  586. }
  587. .nav {
  588. display: flex;
  589. flex-direction: column;
  590. gap: 8px;
  591. margin-bottom: 14px;
  592. }
  593. .navrow {
  594. display: flex;
  595. gap: 8px;
  596. align-items: center;
  597. flex-wrap: wrap;
  598. }
  599. .navlab {
  600. width: 54px;
  601. flex: 0 0 54px;
  602. color: var(--muted);
  603. font-size: 12px;
  604. font-weight: 700;
  605. text-transform: uppercase;
  606. }
  607. .navrow .tab {
  608. border: 1px solid var(--line);
  609. background: #fff;
  610. color: var(--ink);
  611. border-radius: 999px;
  612. padding: 6px 14px;
  613. font-size: 13px;
  614. cursor: pointer;
  615. }
  616. .navrow .tab.on {
  617. background: var(--ink);
  618. color: #fff;
  619. border-color: var(--ink);
  620. }
  621. .navrow .tab .q {
  622. font-family: ui-monospace, Menlo, monospace;
  623. }
  624. .navrow .tab small {
  625. color: var(--muted);
  626. margin-left: 4px;
  627. }
  628. .navrow .tab.on small {
  629. color: #cfd8dc;
  630. }
  631. #navQ .qtab {
  632. max-width: 100%;
  633. white-space: normal;
  634. text-align: left;
  635. line-height: 1.4;
  636. }
  637. .navrow .tab {
  638. white-space: normal;
  639. }
  640. #refresh {
  641. cursor: pointer;
  642. }
  643. /* 1:1 Morandi Matrix Replicated Style Rules */
  644. .btn {
  645. padding: 3px 10px;
  646. border-radius: 14px;
  647. border: 1px solid #d1d5db;
  648. background: #fff;
  649. cursor: pointer;
  650. font-size: .74rem;
  651. color: #444;
  652. }
  653. .g-form .btn.on {
  654. background: #4f46e5;
  655. border-color: #4f46e5;
  656. color: #fff;
  657. }
  658. .g-lens .btn.on {
  659. background: #be185d;
  660. border-color: #be185d;
  661. color: #fff;
  662. }
  663. .g-mod .btn.on {
  664. background: #2e7d32;
  665. border-color: #2e7d32;
  666. color: #fff;
  667. }
  668. .g-tool .btn.on {
  669. background: #c2410c;
  670. border-color: #c2410c;
  671. color: #fff;
  672. }
  673. .g-tier .btn.on {
  674. background: #374151;
  675. border-color: #374151;
  676. color: #fff;
  677. }
  678. .g-mxview .btn.on {
  679. background: var(--mint);
  680. border-color: var(--mint);
  681. color: #fff;
  682. }
  683. .mxwrap {
  684. max-height: 55vh;
  685. overflow: auto;
  686. border: 1px solid var(--line);
  687. border-radius: 0 0 8px 8px;
  688. margin-bottom: 12px;
  689. background: #fff;
  690. position: relative;
  691. }
  692. table#comboMx {
  693. border-collapse: separate;
  694. border-spacing: 0;
  695. background: #fff;
  696. font-size: .65rem;
  697. width: 100%;
  698. }
  699. table#comboMx th,
  700. table#comboMx td {
  701. border-right: 1px solid #f0f1f5;
  702. border-bottom: 1px solid #f0f1f5;
  703. text-align: center;
  704. }
  705. table#comboMx thead th {
  706. position: sticky;
  707. background: #fff;
  708. }
  709. table#comboMx thead tr.l1 th {
  710. top: 0;
  711. z-index: 11;
  712. background: #eef2ff;
  713. color: #4338ca;
  714. font-weight: 700;
  715. font-size: .72rem;
  716. height: 20px;
  717. border-bottom: 1px solid #c7d2fe;
  718. }
  719. table#comboMx thead tr.l2 th {
  720. top: 20px;
  721. z-index: 11;
  722. background: #f5f7ff;
  723. color: #6366f1;
  724. font-weight: 600;
  725. font-size: .66rem;
  726. height: 18px;
  727. border-bottom: 1px solid #e0e7ff;
  728. }
  729. table#comboMx thead tr.leaf th {
  730. top: 38px;
  731. z-index: 9;
  732. background: #fff;
  733. color: #1f2937;
  734. font-size: .66rem;
  735. min-width: 120px;
  736. max-width: 120px;
  737. height: 24px;
  738. padding: 3px;
  739. }
  740. table#comboMx thead .corner {
  741. left: 0;
  742. top: 0;
  743. z-index: 13;
  744. background: #fff;
  745. min-width: 96px;
  746. }
  747. .l1div {
  748. border-left: 3px solid #818cf8 !important;
  749. }
  750. .l2div {
  751. border-left: 1.5px solid #cbd5e8 !important;
  752. }
  753. table#comboMx tbody th.rh {
  754. position: sticky;
  755. left: 0;
  756. z-index: 8;
  757. background: #fffaf0;
  758. color: #92580c;
  759. text-align: left;
  760. padding: 4px 8px;
  761. font-weight: 600;
  762. white-space: nowrap;
  763. min-width: 96px;
  764. border-right: 1px solid #e6e8ef;
  765. }
  766. table#comboMx tbody tr.l1row td {
  767. background: #eef2ff;
  768. color: #4338ca;
  769. font-weight: 700;
  770. font-size: .7rem;
  771. text-align: left;
  772. padding: 3px 8px;
  773. position: sticky;
  774. left: 0;
  775. }
  776. td.cell {
  777. min-width: 120px;
  778. max-width: 120px;
  779. height: 30px;
  780. padding: 2px 5px;
  781. cursor: pointer;
  782. font-family: ui-monospace, Menlo, monospace;
  783. font-size: .65rem;
  784. color: #1f2937;
  785. text-align: left;
  786. line-height: 1.25;
  787. border-left: 4px solid transparent;
  788. overflow: hidden;
  789. position: relative;
  790. }
  791. td.cell:hover {
  792. box-shadow: inset 0 0 0 2px #f59e0b;
  793. }
  794. td.cell.t0 {
  795. border-left-color: #e5e7eb;
  796. background: #fbfbfc;
  797. color: #aeb3bd;
  798. }
  799. td.cell.t1 {
  800. border-left-color: #bbf7d0;
  801. }
  802. td.cell.t2 {
  803. border-left-color: #4ade80;
  804. background: #f3fdf6;
  805. }
  806. td.cell.t3 {
  807. border-left-color: #15803d;
  808. background: #ecfdf3;
  809. }
  810. td.cell.tNA {
  811. border-left-color: #fcd34d;
  812. background: repeating-linear-gradient(45deg, #fff, #fff 4px, #fef9ec 4px, #fef9ec 8px);
  813. color: #b6bac4;
  814. }
  815. td.cell.gq-cell {
  816. color: #15803d;
  817. font-weight: 600;
  818. }
  819. td.cell.rowdim {
  820. opacity: .26;
  821. }
  822. td.cell.hide {
  823. visibility: hidden;
  824. }
  825. td.cell.sel {
  826. outline: 2px solid var(--ink);
  827. outline-offset: -2px;
  828. font-weight: bold;
  829. }
  830. td.cell.rowsel,
  831. td.cell.colsel {
  832. box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .12);
  833. }
  834. td.cell.rowsel.colsel {
  835. box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .22);
  836. }
  837. th.rh.rowsel {
  838. background: rgba(255, 193, 7, .22);
  839. color: var(--ink);
  840. font-weight: 700;
  841. }
  842. thead th.colsel {
  843. background: rgba(255, 193, 7, .22);
  844. color: var(--ink);
  845. font-weight: 700;
  846. }
  847. /* Database hit badge inside matrix cells */
  848. td.cell .hit-badge {
  849. position: absolute;
  850. right: 2px;
  851. top: 2px;
  852. background: #10b981;
  853. color: #fff;
  854. font-size: 9px;
  855. font-weight: bold;
  856. border-radius: 3px;
  857. padding: 0 3px;
  858. line-height: 1.2;
  859. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  860. }
  861. /* Pop-up floating panel styling */
  862. .pop {
  863. position: fixed;
  864. background: #fff;
  865. border: 1px solid #d1d5db;
  866. border-radius: 9px;
  867. box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
  868. padding: 13px 15px;
  869. max-width: 450px;
  870. z-index: 200;
  871. display: none;
  872. font-size: .82rem;
  873. line-height: 1.5;
  874. }
  875. .pop .pt {
  876. font-weight: 700;
  877. color: #1f2937;
  878. margin-bottom: 7px;
  879. display: flex;
  880. gap: 6px;
  881. align-items: center;
  882. flex-wrap: wrap;
  883. padding-right: 18px;
  884. }
  885. .pop .path {
  886. font-size: .66rem;
  887. padding: 1px 6px;
  888. border-radius: 3px;
  889. }
  890. .pop .path.a {
  891. background: #eef2ff;
  892. color: #4338ca;
  893. }
  894. .pop .path.t {
  895. background: #fef3c7;
  896. color: #92400e;
  897. }
  898. .pop .tier {
  899. font-size: .64rem;
  900. padding: 1px 7px;
  901. border-radius: 3px;
  902. font-weight: 600;
  903. }
  904. .tb0 {
  905. background: #e5e7eb;
  906. color: #6b7280;
  907. }
  908. .tb1 {
  909. background: #dcfce7;
  910. color: #166534;
  911. }
  912. .tb2 {
  913. background: #bbf7d0;
  914. color: #14532d;
  915. }
  916. .tb3 {
  917. background: #15803d;
  918. color: #fff;
  919. }
  920. .tbNA {
  921. background: #fef3c7;
  922. color: #92400e;
  923. }
  924. .pop .reason {
  925. background: #fffbeb;
  926. border-left: 3px solid #f59e0b;
  927. padding: 7px 11px;
  928. border-radius: 3px;
  929. color: #78350f;
  930. margin: 7px 0;
  931. font-size: .8rem;
  932. }
  933. .pop .reason.inv {
  934. background: #fef2f2;
  935. border-left-color: #ef4444;
  936. color: #991b1b;
  937. }
  938. .pop .gq {
  939. background: #ecfdf3;
  940. border: 1px solid #a7f3d0;
  941. border-radius: 6px;
  942. padding: 7px 11px;
  943. margin: 7px 0;
  944. font-family: ui-monospace, Menlo, monospace;
  945. font-size: .84rem;
  946. color: #15803d;
  947. }
  948. .pop .gq .t {
  949. font-family: inherit;
  950. font-size: .64rem;
  951. color: #6b7280;
  952. display: block;
  953. margin-bottom: 2px;
  954. }
  955. .pop .small {
  956. font-size: .7rem;
  957. color: #9aa0ad;
  958. margin: 4px 0;
  959. }
  960. .pop .lbl {
  961. font-size: .66rem;
  962. color: #6b7280;
  963. font-weight: 600;
  964. margin: 9px 0 3px;
  965. }
  966. .pop ul {
  967. margin: 0;
  968. padding: 0;
  969. list-style: none;
  970. }
  971. .pop li {
  972. padding: 3px 8px;
  973. background: #f9fafb;
  974. border-radius: 4px;
  975. margin-bottom: 3px;
  976. font-family: ui-monospace, Menlo, monospace;
  977. font-size: .78rem;
  978. color: #1f2937;
  979. }
  980. .pop li.gen {
  981. background: #eef0ff;
  982. color: #3730a3;
  983. }
  984. .pop li.cur {
  985. box-shadow: inset 0 0 0 2px #818cf8;
  986. }
  987. .pop li .fm {
  988. font-size: .62rem;
  989. color: #4f46e5;
  990. margin-right: 6px;
  991. }
  992. .pop .note {
  993. font-size: .7rem;
  994. color: #2e7d32;
  995. margin-top: 6px;
  996. }
  997. .pop .close {
  998. position: absolute;
  999. top: 6px;
  1000. right: 9px;
  1001. cursor: pointer;
  1002. color: #9ca3af;
  1003. font-size: 1.15rem;
  1004. }
  1005. .fac {
  1006. border: 1px solid var(--line);
  1007. background: #fff;
  1008. border-radius: 999px;
  1009. padding: 4px 11px;
  1010. font-size: 12px;
  1011. cursor: pointer;
  1012. color: var(--ink);
  1013. }
  1014. .fac.on {
  1015. background: var(--mint);
  1016. color: #fff;
  1017. border-color: var(--mint);
  1018. }
  1019. .fac small {
  1020. opacity: .65;
  1021. margin-left: 3px;
  1022. }
  1023. #navQ .qtab .hit {
  1024. display: inline-block;
  1025. margin-left: 7px;
  1026. background: var(--soft-mint);
  1027. color: var(--mint);
  1028. border-radius: 999px;
  1029. padding: 0 8px;
  1030. font-size: 11px;
  1031. font-weight: 700;
  1032. }
  1033. #navQ .qtab.on .hit {
  1034. background: rgba(255, 255, 255, .22);
  1035. color: #fff;
  1036. }
  1037. .modal-tabs {
  1038. display: flex;
  1039. gap: 4px;
  1040. padding: 0 16px;
  1041. border-bottom: 1px solid var(--line);
  1042. background: #faf7f1;
  1043. }
  1044. .modal-tab {
  1045. background: transparent;
  1046. border: none;
  1047. border-bottom: 3px solid transparent;
  1048. border-radius: 0;
  1049. padding: 10px 16px;
  1050. font-size: 14px;
  1051. font-weight: 600;
  1052. color: var(--muted);
  1053. cursor: pointer;
  1054. transition: all 0.2s ease;
  1055. }
  1056. .modal-tab:hover {
  1057. color: var(--ink);
  1058. background: rgba(0, 0, 0, 0.02);
  1059. }
  1060. .modal-tab.active {
  1061. color: var(--mint);
  1062. border-bottom-color: var(--mint);
  1063. }
  1064. /* 新版评估分数卡片可视化样式 */
  1065. .sc-card {
  1066. background: #fff;
  1067. border: 1px solid var(--line);
  1068. border-radius: 12px;
  1069. padding: 16px 20px;
  1070. margin-bottom: 16px;
  1071. box-shadow: 0 4px 15px rgba(0,0,0,0.02);
  1072. }
  1073. .sc-card-head {
  1074. display: flex;
  1075. justify-content: space-between;
  1076. align-items: center;
  1077. margin-bottom: 14px;
  1078. border-bottom: 1px solid #f3f0ea;
  1079. padding-bottom: 10px;
  1080. }
  1081. .sc-card-head .title {
  1082. font-size: 16px;
  1083. font-weight: 700;
  1084. display: flex;
  1085. align-items: center;
  1086. gap: 8px;
  1087. }
  1088. .sc-card-head .badge {
  1089. background: #eef2ff;
  1090. color: #2563eb;
  1091. font-size: 11px;
  1092. padding: 2px 6px;
  1093. border-radius: 4px;
  1094. font-weight: 700;
  1095. }
  1096. .sc-card-head .avg-score {
  1097. font-size: 14px;
  1098. color: var(--muted);
  1099. font-weight: 600;
  1100. }
  1101. .sc-card-head .avg-score strong {
  1102. font-size: 26px;
  1103. color: #2563eb;
  1104. font-weight: 800;
  1105. margin-left: 6px;
  1106. }
  1107. .sc-sub-header {
  1108. font-size: 12px;
  1109. color: var(--muted);
  1110. font-weight: 700;
  1111. margin: 14px 0 8px;
  1112. border-bottom: 1px dashed #f0ebd8;
  1113. padding-bottom: 4px;
  1114. text-transform: uppercase;
  1115. letter-spacing: 0.5px;
  1116. }
  1117. .sc-row {
  1118. display: flex;
  1119. justify-content: space-between;
  1120. align-items: center;
  1121. padding: 6px 0;
  1122. font-size: 13.5px;
  1123. gap: 10px;
  1124. }
  1125. .sc-row .label {
  1126. color: var(--ink);
  1127. font-weight: 500;
  1128. flex: 1;
  1129. min-width: 100px;
  1130. word-break: break-all;
  1131. }
  1132. .sc-row .bar-wrap {
  1133. display: flex;
  1134. align-items: center;
  1135. gap: 10px;
  1136. width: 170px;
  1137. flex-shrink: 0;
  1138. }
  1139. .sc-row .bar {
  1140. height: 6px;
  1141. background: #eee7dc;
  1142. border-radius: 999px;
  1143. flex: 1;
  1144. overflow: hidden;
  1145. }
  1146. .sc-row .bar-fill {
  1147. height: 100%;
  1148. background: #2563eb;
  1149. border-radius: 999px;
  1150. width: calc(var(--v) * 10%);
  1151. }
  1152. .sc-row .value {
  1153. font-weight: 700;
  1154. font-size: 13.5px;
  1155. width: 20px;
  1156. text-align: right;
  1157. }
  1158. .sc-row .info-icon {
  1159. cursor: pointer;
  1160. color: #9ca3af;
  1161. transition: color 0.15s ease;
  1162. font-size: 13px;
  1163. user-select: none;
  1164. }
  1165. .sc-row .info-icon:hover {
  1166. color: #3b82f6;
  1167. }
  1168. </style>
  1169. </head>
  1170. <body>
  1171. <header>
  1172. <div class="wrap">
  1173. <div class="eyebrow">Content Search · runs/ 实时 · query → 形式 → 渠道</div>
  1174. <h1>搜索评估 · 案例总览</h1>
  1175. <p class="lede" id="lede" style="margin:0">加载中…</p>
  1176. </div>
  1177. </header>
  1178. <main>
  1179. <div class="wrap">
  1180. <div class="nav">
  1181. <div class="navrow" style="margin-bottom:4px;">
  1182. <span class="navlab">组合矩阵</span>
  1183. <span style="color:var(--muted);font-size:12px;flex:1">行=类型 列=动作 格子色=评分,角标=帖子数/工序数。点击选组合且弹出详情。</span>
  1184. <span class="g-mxview" style="display:inline-flex;gap:4px;margin-right:12px;align-items:center;">
  1185. <button class="btn on" id="btnMxFull" onclick="setMatrixView('full')">完整矩阵</button>
  1186. <button class="btn" id="btnMxHits" onclick="setMatrixView('hits')">只看帖子命中</button>
  1187. <button class="btn" id="btnMxProcedures" onclick="setMatrixView('procedures')">只看工序</button>
  1188. </span>
  1189. <button id="refresh" onclick="loadData(true)">↻ 刷新 runs</button>
  1190. </div>
  1191. <div class="mx-header"
  1192. style="background:#fff; border: 1px solid var(--line); border-radius: 8px 8px 0 0; padding:12px 16px 8px; border-bottom: none;">
  1193. <h4
  1194. style="margin:0 0 4px; font-size:13px; color:#1f2937; display:flex; justify-content:space-between; align-items:center; font-weight:700;">
  1195. <span>动作 × 类型 · 组合矩阵 <span style="font-weight:400;font-size:11px;color:#9aa0ad">基于 <b
  1196. id="gm">gemini-3.1-flash-lite</b></span></span>
  1197. <span class="legend" style="display:flex;gap:9px;align-items:center;color:#6b7280;font-size:11px;">
  1198. <span><span class="sw"
  1199. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#15803d;vertical-align:middle;margin-right:3px;"></span>高
  1200. · <b id="s3" style="color:#15803d;">0</b></span>
  1201. <span><span class="sw"
  1202. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#4ade80;vertical-align:middle;margin-right:3px;"></span>中
  1203. · <b id="s2" style="color:#4ade80;">0</b></span>
  1204. <span><span class="sw"
  1205. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#bbf7d0;vertical-align:middle;margin-right:3px;"></span>低
  1206. · <b id="s1" style="color:#10b981;">0</b></span>
  1207. <span><span class="sw"
  1208. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#e5e7eb;vertical-align:middle;margin-right:3px;"></span>无效
  1209. · <b id="s0" style="color:#9ca3af;">0</b></span>
  1210. <span><span class="sw"
  1211. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#fcd34d;vertical-align:middle;margin-right:3px;"></span>未判
  1212. · <b id="sna" style="color:#b87918;">0</b></span>
  1213. </span>
  1214. </h4>
  1215. <div class="sub" style="font-size:11px;color:#6b7280;margin-bottom:8px;">
  1216. 共 1350 格 · 形式①原词不替换 · ②句子套模板 · ③同义池替换。
  1217. </div>
  1218. <div class="ctl"
  1219. style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:11px;border-top:1px solid #f3f4f6;padding-top:8px;">
  1220. <span class="lab" style="color:#9aa0ad;font-weight:600;">query生成方式</span>
  1221. <span class="g-form">
  1222. <button class="btn on" data-k="form" data-v="A">①原词</button>
  1223. <button class="btn" data-k="form" data-v="B">②句子</button>
  1224. <button class="btn" data-k="form" data-v="C">③同义</button>
  1225. </span>
  1226. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">知识类型</span>
  1227. <span class="g-lens">
  1228. <button class="btn on" data-k="lens" data-v="工序">工序</button>
  1229. <button class="btn" data-k="lens" data-v="工具">工具</button>
  1230. <button class="btn" data-k="lens" data-v="能力">能力</button>
  1231. </span>
  1232. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">约束·工具类型</span>
  1233. <span class="g-tool">
  1234. <button class="btn on" data-k="tool" data-v="">无</button>
  1235. <span id="toolBtns"></span>
  1236. </span>
  1237. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">搜索优先级</span>
  1238. <span class="g-tier">
  1239. <button class="btn on" data-k="tier" data-v="0">全部</button>
  1240. <button class="btn" data-k="tier" data-v="2">≥中</button>
  1241. <button class="btn" data-k="tier" data-v="3">仅高</button>
  1242. </span>
  1243. </div>
  1244. </div>
  1245. <div class="mxwrap"
  1246. style="max-height:55vh; overflow:auto; border:1px solid var(--line); border-radius: 0 0 8px 8px; margin-bottom:12px; background:#fff; position:relative;">
  1247. <table id="comboMx"></table>
  1248. </div>
  1249. <div class="stats" id="stats" style="margin-top:0; margin-bottom:12px;"></div>
  1250. <div class="pop" id="pop"></div>
  1251. <div class="navrow"><span class="navlab">渠道</span>
  1252. <div id="navC" style="display:flex;gap:8px;flex-wrap:wrap"></div>
  1253. </div>
  1254. </div>
  1255. <div class="toolbar">
  1256. <div class="filters"></div>
  1257. <!-- 动态相关性过滤阈值 -->
  1258. <div class="threshold-control" style="display: flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 4px 12px; font-size: 13px; font-weight: 600; box-shadow: var(--shadow);">
  1259. <span style="color: var(--muted); font-size: 12px; font-weight: 700;">相关性过滤阈值:</span>
  1260. <input type="number" id="relThreshold" min="0" max="10" step="0.5" value="4.0"
  1261. oninput="renderGrid(); renderHead();"
  1262. style="width: 55px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 6px; font-weight: 700; text-align: center; color: #2563eb; outline: none; transition: border-color 0.2s;"
  1263. onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#d1d5db'">
  1264. </div>
  1265. <button id="reevalBtn" onclick="reevalCurrentQuery()" title="只对当前 query 的所有 form/帖子复评(不重新搜索)">♻️ 重评当前
  1266. query</button>
  1267. <select id="sort">
  1268. <option value="score">按综合分排序</option>
  1269. <option value="date">按发布时间排序</option>
  1270. <option value="platform">按平台排序</option>
  1271. </select>
  1272. </div>
  1273. <div class="grid" id="grid"></div>
  1274. </div>
  1275. </main>
  1276. <dialog id="detailDialog">
  1277. <div class="modal-head">
  1278. <div>
  1279. <div id="modalMeta" class="meta"></div>
  1280. <h3 id="modalTitle"></h3>
  1281. </div>
  1282. <div style="display: flex; gap: 8px; flex-shrink: 0;">
  1283. <button id="fullscreenBtn" onclick="toggleFullscreen()">全屏</button>
  1284. <button onclick="detailDialog.close()">关闭</button>
  1285. </div>
  1286. </div>
  1287. <div class="modal-tabs" id="modalTabs" style="display: none;">
  1288. <button class="modal-tab active" onclick="switchModalTab('detail')" id="tabDetailBtn">帖子详情</button>
  1289. <button class="modal-tab" onclick="switchModalTab('procedure')" id="tabProcedureBtn">对应工序</button>
  1290. </div>
  1291. <div class="modal-content" id="modalContentDetail">
  1292. <section>
  1293. <div class="rubric-note" id="modalReason"></div>
  1294. <div class="section-title">抓取文本节选</div>
  1295. <div class="raw" id="modalText"></div>
  1296. <div class="section-title">图片预览</div>
  1297. <div class="images" id="modalImages"></div>
  1298. </section>
  1299. <aside>
  1300. <div class="section-title" style="display: flex; justify-content: space-between; align-items: center;">
  1301. <span>评分详情</span>
  1302. <span id="modalOverallScore" style="font-size: 13.5px; font-weight: 700; color: var(--muted); display: flex; align-items: center; gap: 4px; background: #faf7f1; border: 1px solid var(--line); padding: 3px 10px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.01);">
  1303. 综合评分 <strong style="font-size: 19px; color: #2563eb; font-weight: 900; line-height: 1;" id="modalOverallScoreVal">—</strong>
  1304. </span>
  1305. </div>
  1306. <div class="scores" id="modalScores"></div>
  1307. <div class="section-title">类型 / 命中 query</div>
  1308. <div class="tags" id="modalTags"></div>
  1309. </aside>
  1310. </div>
  1311. <div id="modalContentProcedure" style="display: none; height: 600px;">
  1312. <iframe id="procedureIframe" style="width: 100%; height: 100%; border: none; background: #fff;" referrerpolicy="no-referrer"></iframe>
  1313. </div>
  1314. </dialog>
  1315. <script>
  1316. let DATA = { queries: [], actions: [], types: [], matrix: [] }, st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full' }, VIEW = [];
  1317. function esc(s) { return (s === undefined || s === null ? "" : String(s)).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); }
  1318. const ACTIONS = [{ "name": "检索", "l1": "获取", "l2": "搜索" }, { "name": "下载", "l1": "获取", "l2": "搜索" }, { "name": "调取", "l1": "获取", "l2": "查询" }, { "name": "上传", "l1": "获取", "l2": "录入" }, { "name": "拍摄", "l1": "获取", "l2": "录入" }, { "name": "录音", "l1": "获取", "l2": "录入" }, { "name": "键入", "l1": "获取", "l2": "录入" }, { "name": "选取", "l1": "获取", "l2": "引用" }, { "name": "裁切", "l1": "提取", "l2": "物理提取" }, { "name": "抠取", "l1": "提取", "l2": "物理提取" }, { "name": "抽帧", "l1": "提取", "l2": "物理提取" }, { "name": "识别", "l1": "提取", "l2": "化学提取" }, { "name": "反推", "l1": "提取", "l2": "化学提取" }, { "name": "解构", "l1": "提取", "l2": "化学提取" }, { "name": "元素生成", "l1": "生成", "l2": "元素生成" }, { "name": "数组生成", "l1": "生成", "l2": "关系生成" }, { "name": "结构生成", "l1": "生成", "l2": "关系生成" }, { "name": "添加", "l1": "修改", "l2": "增" }, { "name": "叠加", "l1": "修改", "l2": "增" }, { "name": "抹除", "l1": "修改", "l2": "删" }, { "name": "剪除", "l1": "修改", "l2": "删" }, { "name": "重述", "l1": "修改", "l2": "变" }, { "name": "风格化", "l1": "修改", "l2": "变" }, { "name": "转换", "l1": "修改", "l2": "变" }, { "name": "替换", "l1": "修改", "l2": "变" }, { "name": "调整", "l1": "修改", "l2": "变" }, { "name": "增强", "l1": "修改", "l2": "变" }];
  1319. const TYPES = [{ "name": "提示词", "l1": "程序控制类型", "l2": "指令" }, { "name": "负向提示词", "l1": "程序控制类型", "l2": "指令" }, { "name": "描述", "l1": "程序控制类型", "l2": "指令" }, { "name": "生成参数", "l1": "程序控制类型", "l2": "参数" }, { "name": "规格参数", "l1": "程序控制类型", "l2": "参数" }, { "name": "模型权重", "l1": "程序控制类型", "l2": "参数" }, { "name": "评分", "l1": "程序控制类型", "l2": "评估" }, { "name": "评语", "l1": "程序控制类型", "l2": "评估" }, { "name": "工作流", "l1": "程序控制类型", "l2": "流程" }, { "name": "批处理", "l1": "程序控制类型", "l2": "流程" }, { "name": "数字人", "l1": "数据复用类型", "l2": "原子" }, { "name": "版式", "l1": "数据复用类型", "l2": "原子" }, { "name": "模板", "l1": "数据复用类型", "l2": "序列" }, { "name": "参考图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "参考视频", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "参考音频", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "对标内容", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "分镜图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "转场", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "蒙版", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "控制图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "运动轨迹", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "滤镜", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "构图布局", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "截图", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "视频片段", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "转场片段", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "关键帧", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "音效", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "特效", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "大纲", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "分镜脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "剪辑脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "配音文案", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "底图", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "样图", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "分镜视频", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "图层组合", "l1": "内容类型", "l2": "半成品/组合" }, { "name": "拼图", "l1": "内容类型", "l2": "半成品/组合" }, { "name": "歌词", "l1": "内容类型", "l2": "准成品" }, { "name": "配音", "l1": "内容类型", "l2": "准成品" }, { "name": "BGM", "l1": "内容类型", "l2": "准成品" }, { "name": "字幕", "l1": "内容类型", "l2": "准成品" }, { "name": "标题", "l1": "内容类型", "l2": "准成品" }, { "name": "正文", "l1": "内容类型", "l2": "准成品" }, { "name": "成品图", "l1": "内容类型", "l2": "成品" }, { "name": "视频成品", "l1": "内容类型", "l2": "成品" }, { "name": "合成图", "l1": "内容类型", "l2": "成品" }, { "name": "知识库", "l1": "知识类型", "l2": "知识库" }];
  1320. const POOLS = { "action_leaves": { "检索": ["找", "搜", "哪里找", "在哪找", "搜索"], "下载": ["下载", "怎么下载", "获取", "导入"], "调取": ["复用", "调用", "套用", "加载"], "上传": ["上传", "导入", "怎么导入", "用自己的"], "拍摄": ["拍", "拍摄", "录制", "录屏", "怎么拍"], "录音": ["录音", "录", "怎么录", "录制"], "键入": ["写", "输入", "怎么写", "编写"], "选取": ["挑选", "怎么选", "筛选", "选"], "裁切": ["裁剪", "截取", "怎么裁", "切片"], "抠取": ["抠图", "抠", "分割", "怎么抠"], "抽帧": ["抽帧", "提取帧", "怎么抽帧", "导出帧"], "识别": ["识别", "检测", "OCR", "提取文字", "转录"], "反推": ["反推", "分析", "推断", "怎么反推"], "解构": ["拆解", "解构", "分析结构"], "元素生成": ["生成", "制作", "怎么做", "做", "创作"], "数组生成": ["批量生成", "生成多张", "怎么批量", "批量做"], "结构生成": ["生成", "搭建", "怎么生成", "构建"], "添加": ["添加", "加", "怎么加", "加上"], "叠加": ["叠加", "合成", "叠", "加"], "抹除": ["去除", "去掉", "怎么去掉", "抹除", "消除"], "剪除": ["剪掉", "删掉", "怎么剪", "截短"], "重述": ["改写", "润色", "怎么改", "重写"], "风格化": ["风格化", "转风格", "风格迁移", "怎么转风格"], "转换": ["转换", "转", "怎么转", "转格式"], "替换": ["替换", "换", "怎么换", "替换成"], "调整": ["调整", "调", "怎么调", "优化"], "增强": ["增强", "提升画质", "怎么增强", "修复", "超分"] }, "types": { "提示词": ["提示词", "prompt", "咒语"], "负向提示词": ["负向提示词", "反向提示词", "negative prompt"], "描述": ["描述", "描述词", "提示描述"], "生成参数": ["参数", "出图参数", "生成参数", "seed"], "规格参数": ["尺寸", "分辨率", "画幅", "规格"], "模型权重": ["模型", "LoRA", "底模", "checkpoint"], "评分": ["评分", "打分", "评级"], "评语": ["评语", "点评", "反馈", "修改意见"], "工作流": ["工作流", "workflow", "流程图", "节点"], "批处理": ["批处理", "批量", "batch"], "数字人": ["数字人", "虚拟人", "数字分身", "AI分身"], "版式": ["版式", "排版", "版面", "layout"], "模板": ["模板", "template", "套版"], "参考图": ["参考图", "参考", "垫图", "ref"], "参考视频": ["参考视频", "参考片", "参考素材"], "参考音频": ["参考音频", "参考音色", "音色样本"], "对标内容": ["对标", "爆款参考", "竞品", "标杆案例"], "分镜图": ["分镜图", "分镜", "故事板", "storyboard"], "转场": ["转场", "转场效果", "transition"], "蒙版": ["蒙版", "抠像", "遮罩", "mask"], "控制图": ["控制图", "ControlNet", "结构图", "线稿"], "运动轨迹": ["运镜", "运动轨迹", "运动笔刷", "motion brush"], "滤镜": ["滤镜", "filter", "调色", "LUT"], "构图布局": ["构图", "布局", "构图布局"], "截图": ["截图", "屏幕截图", "截屏"], "视频片段": ["视频片段", "片段", "素材", "clip"], "转场片段": ["转场片段", "转场素材"], "关键帧": ["关键帧", "帧", "视频帧"], "音效": ["音效", "声音", "SFX", "音效素材"], "特效": ["特效", "视频特效", "VFX", "effect"], "大纲": ["大纲", "内容大纲", "提纲"], "脚本": ["脚本", "文案脚本", "剧本"], "分镜脚本": ["分镜脚本", "分镜表", "分镜"], "剪辑脚本": ["剪辑脚本", "剪辑表", "卡点表", "时间轴"], "配音文案": ["配音文案", "口播文案", "旁白文案"], "底图": ["底图", "背景图", "打底图"], "样图": ["样图", "效果图", "候选图", "draft"], "分镜视频": ["分镜视频", "分镜片段"], "图层组合": ["图层组合", "图层", "图层合成"], "拼图": ["拼图", "九宫格", "长图", "对比图"], "歌词": ["歌词", "词", "lyrics"], "配音": ["配音", "旁白", "解说", "人声"], "BGM": ["BGM", "背景音乐", "配乐"], "字幕": ["字幕", "视频字幕", "字幕条"], "标题": ["标题", "文案标题", "爆款标题"], "正文": ["正文", "文案", "内容正文"], "成品图": ["成品图", "出图", "图", "海报"], "视频成品": ["视频成品", "成片", "视频"], "合成图": ["合成图", "拼合图", "融合图", "合成"], "知识库": ["知识库", "资料库", "knowledge base"] }, "knowledge": { "工序": { "单步": ["教程", "流程", "步骤", "怎么做", "方法", "教学"], "全程": ["完整流程", "全流程", "pipeline", "SOP"] }, "能力": { "标记": ["一键", "自动", "直出", "秒出"], "达成": ["同款", "复刻", "效果"], "载体": ["提示词", "参数", "预设", "公式"] }, "工具": { "发现": ["用什么软件", "用什么工具", "工具推荐", "哪个好用", "有哪些工具"] } }, "tool_type": { "AI 模型": ["AI"], "桌面 APP": ["软件", "电脑端"], "云端 Web": ["在线", "网页版"], "API·CLI": ["代码", "命令行"], "插件扩展": ["插件"] } };
  1321. const TOOL_TYPES = ["AI 模型", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"];
  1322. const GMODEL = "gemini-3.1-flash-lite";
  1323. const AL = POOLS.action_leaves, TP = POOLS.types, KN = POOLS.knowledge, TQ = POOLS.tool_type;
  1324. function aPool(a) { return AL[a] || [a]; }
  1325. function tPool(t) { return TP[t] || [t]; }
  1326. function pick(arr, i) { return arr[Math.min(i, arr.length - 1)]; }
  1327. function toolPrefix(lens) { return (st.tools.length && lens !== '工具') ? st.tools.map(t => TQ[t][0]).join('/') + ' ' : ''; }
  1328. function genForms(leaf, ty, lens, tq) {
  1329. tq = tq || ''; const aP = aPool(leaf), tP = tPool(ty);
  1330. const aNat = aP[0], tNat = tP[0], aSyn = pick(aP, 1), tSyn = pick(tP, 1), K = KN[lens];
  1331. if (lens === '工序') {
  1332. const s = K['单步'];
  1333. return [['①原词', `${tq}${leaf} ${ty} 流程`], ['②句子', `${tq}怎么${aNat}${tNat}`], ['③同义', `${tq}${aSyn} ${tSyn} ${pick(s, 2)}`]];
  1334. }
  1335. if (lens === '能力') {
  1336. const mk = K['标记'];
  1337. return [['①原词', `${tq}${leaf} ${ty} 技巧`], ['②句子', `${tq}有没有能${mk[0]}${aNat}${tNat}的功能`], ['③同义', `${tq}${pick(mk, 1)} ${aSyn}${tSyn}`]];
  1338. }
  1339. const fd = K['发现'];
  1340. return [['①原词', `${leaf} ${ty} 工具`], ['②句子', `${aNat}${tNat}用什么软件好`], ['③同义', `${aSyn} ${tSyn} ${pick(fd, 2)}`]];
  1341. }
  1342. const FIDX = { A: 0, B: 1, C: 2 };
  1343. function genQ(leaf, ty) { return genForms(leaf, ty, st.lens, toolPrefix(st.lens))[FIDX[st.form]][1]; }
  1344. // 兼容老版 1-5 维度的英文和中文对应标签
  1345. const commonLabels = { relevance: "相关性", result_quality: "成品质量", credibility: "可信度", novelty_coverage: "新增覆盖", concrete_use_case: "具体用例", completeness: "流程完整", step_structure: "步骤结构", step_reproducibility: "可复现性", capability_definition: "能力定义", implementation_depth: "实现深度", boundary_failure_eval: "边界/踩坑", generality: "通用性", capability_coverage: "工具覆盖", effective_comparison: "有效对比", param_specificity: "参数具体", worked_example: "示例完整", version_limits: "限制说明" };
  1346. const filterLabels = { production_relevance: "制作相关性", recency_hard: "发布时效", overall: "综合均分" };
  1347. const filterMax = { production_relevance: 3, recency_hard: 3, overall: 5 };
  1348. // 新版 0-10 分数维度的中文标签
  1349. const newLabels = {
  1350. relevance_production: "和内容制作知识相关",
  1351. relevance_query: "和 query 相关",
  1352. recency: "时效性",
  1353. popularity: "热度性",
  1354. feedback: "评论反馈",
  1355. realism: "真实感 (非AI)",
  1356. expressiveness: "表现力",
  1357. procedure_completeness: "流程完整性",
  1358. procedure_input: "输入完整性",
  1359. procedure_implementation: "实现完整性",
  1360. procedure_output: "输出完整性",
  1361. procedure_generality: "泛化性",
  1362. step_input: "输入完整性",
  1363. step_implementation: "实现完整性",
  1364. step_output: "输出完整性",
  1365. step_generality: "泛化性",
  1366. tool_boundary: "能力边界覆盖",
  1367. tool_comparison: "有效比较",
  1368. tool_specificity: "参数/接口具体性",
  1369. tool_example: "实操示例",
  1370. tool_limits: "版本&限制"
  1371. };
  1372. const scoreGroupsOld = [
  1373. { id: "filter", title: "过滤指标", short: "过滤", hint: "独立于通用维度·过滤逻辑待定", topLevelKeys: ["production_relevance", "recency_hard", "overall"] },
  1374. { id: "common", title: "通用维度", short: "通用", keys: ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"] },
  1375. { id: "procedure", title: "工序维度", short: "工序", keys: ["completeness", "step_structure", "step_reproducibility"] },
  1376. { id: "step", title: "步骤维度", short: "步骤", keys: ["capability_definition", "implementation_depth", "boundary_failure_eval", "generality"] },
  1377. { id: "tool", title: "工具维度", short: "工具", keys: ["capability_coverage", "effective_comparison", "param_specificity", "worked_example", "version_limits"] }
  1378. ];
  1379. const scoreGroupsNew = [
  1380. { id: "relevance", title: "相关性", short: "相关", keys: ["relevance_production", "relevance_query"] },
  1381. { id: "fixed", title: "固定维度", short: "固定", keys: ["recency", "popularity", "feedback"] },
  1382. { id: "usecase", title: "用例维度", short: "用例", keys: ["realism", "expressiveness"] },
  1383. { id: "dynamic", title: "动态维度", short: "动态", keys: [] }
  1384. ];
  1385. function makeRow(label, scoreKey, it) {
  1386. const rawV = it.scores[scoreKey];
  1387. const v = (rawV !== undefined && rawV !== null) ? parseFloat(rawV) : NaN;
  1388. const hasScore = !isNaN(v);
  1389. const valStr = hasScore ? (Number.isInteger(v) ? v : v.toFixed(1)) : '-';
  1390. const barV = hasScore ? v : 0;
  1391. const reason = it.score_reasons ? it.score_reasons[scoreKey] : '';
  1392. const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(label)}', '${esc(scoreKey)}')" title="点击定格查看评判理由" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7; font-size: 13px; font-weight: normal; user-select: none;">ⓘ</span>` : '';
  1393. return `
  1394. <div class="sc-row ${!hasScore ? 'missing' : ''}">
  1395. <span class="label">${esc(label)}</span>
  1396. <div class="bar-wrap">
  1397. <div class="bar">
  1398. <div class="bar-fill" style="--v: ${barV}"></div>
  1399. </div>
  1400. <span class="value">${valStr}</span>
  1401. ${infoIcon}
  1402. </div>
  1403. </div>
  1404. `;
  1405. }
  1406. function getQualityAverage(it) {
  1407. if (!it.scores) return null;
  1408. const keys = ["recency", "popularity", "feedback", "realism", "expressiveness"];
  1409. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1410. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1411. }
  1412. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1413. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1414. }
  1415. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1416. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1417. }
  1418. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1419. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1420. }
  1421. function renderNewScores(it) {
  1422. // 1. Relevance Card
  1423. const relAvg = groupAverage(it, scoreGroupsNew[0], true);
  1424. const relAvgStr = relAvg !== null ? relAvg.toFixed(1) : 'N/A';
  1425. let relevanceHtml = `
  1426. <div class="sc-card">
  1427. <div class="sc-card-head">
  1428. <div class="title"><span class="badge">01</span>相关性</div>
  1429. <div class="avg-score">avg <strong>${relAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
  1430. </div>
  1431. <div class="sc-card-body">
  1432. ${makeRow("和内容制作知识相关", "relevance_production", it)}
  1433. ${makeRow("和 query 相关", "relevance_query", it)}
  1434. </div>
  1435. </div>
  1436. `;
  1437. // 2. Quality Card
  1438. const qualAvg = getQualityAverage(it);
  1439. const qualAvgStr = qualAvg !== null ? qualAvg.toFixed(1) : 'N/A';
  1440. let qualityHtml = `
  1441. <div class="sc-card">
  1442. <div class="sc-card-head">
  1443. <div class="title"><span class="badge">02</span>质量</div>
  1444. <div class="avg-score">avg <strong>${qualAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
  1445. </div>
  1446. <div class="sc-card-body">
  1447. <div class="sc-sub-header">固定维度</div>
  1448. ${makeRow("时效性", "recency", it)}
  1449. ${makeRow("热度性", "popularity", it)}
  1450. ${makeRow("评论反馈", "feedback", it)}
  1451. <div class="sc-sub-header">用例</div>
  1452. ${makeRow("真实感 (非AI)", "realism", it)}
  1453. ${makeRow("表现力", "expressiveness", it)}
  1454. `;
  1455. // Dynamic Dimensions
  1456. let dynamicHtml = '';
  1457. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1458. dynamicHtml += `
  1459. <div class="sc-sub-header">工序</div>
  1460. ${makeRow("流程完整性", "procedure_completeness", it)}
  1461. ${makeRow("输入完整性", "procedure_input", it)}
  1462. ${makeRow("实现完整性", "procedure_implementation", it)}
  1463. ${makeRow("输出完整性", "procedure_output", it)}
  1464. ${makeRow("泛化性", "procedure_generality", it)}
  1465. `;
  1466. }
  1467. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1468. dynamicHtml += `
  1469. <div class="sc-sub-header">能力</div>
  1470. ${makeRow("输入完整性", "step_input", it)}
  1471. ${makeRow("实现完整性", "step_implementation", it)}
  1472. ${makeRow("输出完整性", "step_output", it)}
  1473. ${makeRow("泛化性", "step_generality", it)}
  1474. `;
  1475. }
  1476. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1477. dynamicHtml += `
  1478. <div class="sc-sub-header">工具</div>
  1479. ${makeRow("能力边界覆盖", "tool_boundary", it)}
  1480. ${makeRow("有效比较", "tool_comparison", it)}
  1481. ${makeRow("参数/接口具体性", "tool_specificity", it)}
  1482. ${makeRow("实操示例", "tool_example", it)}
  1483. ${makeRow("版本&限制", "tool_limits", it)}
  1484. `;
  1485. }
  1486. qualityHtml += dynamicHtml + `
  1487. </div>
  1488. </div>
  1489. `;
  1490. return relevanceHtml + qualityHtml;
  1491. }
  1492. function isItemDiscarded(it) {
  1493. if (it.anomaly) return false;
  1494. const input = document.getElementById("relThreshold");
  1495. const userThreshold = input ? parseFloat(input.value) : NaN;
  1496. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  1497. let isDiscard = false;
  1498. // 1. Relevance check
  1499. const relVal = it.production_relevance !== null && it.production_relevance !== undefined ? parseFloat(it.production_relevance) : null;
  1500. if (relVal !== null && !isNaN(relVal)) {
  1501. const activeThreshold = !isNaN(userThreshold) ? userThreshold : (isNewSchema ? 4.0 : 2.0);
  1502. if (relVal < activeThreshold) {
  1503. isDiscard = true;
  1504. }
  1505. }
  1506. // 2. Recency check
  1507. if (it.recency_hard !== null && it.recency_hard !== undefined && it.recency_hard < 2) {
  1508. isDiscard = true;
  1509. }
  1510. // 3. Overall average check
  1511. if (it.overall !== null && it.overall !== undefined) {
  1512. const threshold_ov = isNewSchema ? 6.0 : 3.0;
  1513. if (it.overall < threshold_ov) {
  1514. isDiscard = true;
  1515. }
  1516. }
  1517. return isDiscard;
  1518. }
  1519. function updateThresholdLimits() {
  1520. const f = curForm();
  1521. const input = document.getElementById("relThreshold");
  1522. if (!f || !f.results || f.results.length === 0 || !input) return;
  1523. const isNew = f.results.some(r => r.scores && (r.scores.relevance_production !== undefined || r.scores.relevance_query !== undefined));
  1524. const schemaKey = isNew ? "new" : "old";
  1525. if (input.dataset.schema !== schemaKey) {
  1526. input.dataset.schema = schemaKey;
  1527. input.max = isNew ? "10" : "5";
  1528. input.value = isNew ? "4.0" : "2.0";
  1529. input.step = isNew ? "0.5" : "1";
  1530. }
  1531. }
  1532. const PLATC = { xhs: "小红书", gzh: "公众号", zhihu: "知乎", x: "X", bili: "B站", douyin: "抖音", sph: "视频号", youtube: "YouTube", github: "GitHub", toutiao: "头条", weibo: "微博" };
  1533. const FN = { A: "原词", B: "句子", C: "同义" };
  1534. const NOIMG = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='400'%3E%3Crect width='600' height='400' fill='%23eee7dc'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%236f6961' font-size='28'%3ENo image%3C/text%3E%3C/svg%3E";
  1535. function curForm() { return st.qi === -1 ? null : (DATA.queries[st.qi] ? DATA.queries[st.qi].forms[st.fi] : null); }
  1536. function groupAverage(it, g, isNewSchema) {
  1537. if (!it.scores) return null;
  1538. let keys = g.keys;
  1539. if (isNewSchema && g.id === "dynamic") {
  1540. keys = [];
  1541. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1542. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1543. }
  1544. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1545. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1546. }
  1547. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1548. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1549. }
  1550. }
  1551. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1552. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1553. }
  1554. function fmt(v) { return v === null ? "N/A" : v.toFixed(1); }
  1555. function groupSnapshot(it) {
  1556. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  1557. if (isNewSchema) {
  1558. const relAvg = groupAverage(it, scoreGroupsNew[0], true);
  1559. const qualAvg = getQualityAverage(it);
  1560. return `
  1561. <div class="group-pill"><span>相关</span><strong>${fmt(relAvg)}</strong></div>
  1562. <div class="group-pill"><span>质量</span><strong>${fmt(qualAvg)}</strong></div>
  1563. `;
  1564. }
  1565. const groups = scoreGroupsOld;
  1566. return groups.map(g => {
  1567. if (!isNewSchema && g.id === 'filter') {
  1568. const pr = it.production_relevance, rh = it.recency_hard, ov = it.overall;
  1569. const f = (v, max) => (v == null || !Number.isFinite(v)) ? '-' : (max === 5 ? (typeof v === 'number' ? v.toFixed(1) : v) : v);
  1570. return `<div class="group-pill" title="${g.hint || ''}"><span>${g.short}</span><strong style="font-size:12px;">${f(pr, 3)}·${f(rh, 3)}·${f(ov, 5)}</strong></div>`;
  1571. }
  1572. const a = groupAverage(it, g, isNewSchema);
  1573. return `<div class="group-pill"><span>${g.short}</span><strong>${fmt(a)}</strong></div>`;
  1574. }).join("");
  1575. }
  1576. // filter 组:数据从 it 顶层、量程按 filterMax 归一化到 5 量程(meter 的 --v*20% 公式不动)
  1577. function renderScoreGroup(it, g) {
  1578. const isFilter = g.id === 'filter';
  1579. const keys = g.topLevelKeys || g.keys;
  1580. const src = isFilter ? it : it.scores;
  1581. if (!src) return '';
  1582. const labels = isFilter ? filterLabels : commonLabels;
  1583. const rows = keys.map(k => {
  1584. const v = src[k];
  1585. const m = !Number.isFinite(v);
  1586. const max = isFilter ? (filterMax[k] || 5) : 5;
  1587. const meterV = m ? 0 : (v * 5 / max);
  1588. const suffix = isFilter && max !== 5 ? `/${max}` : '';
  1589. const valStr = m ? '-' : (typeof v === 'number' ? (Number.isInteger(v) ? v : v.toFixed(1)) : v);
  1590. const reason = (it.score_reasons) ? it.score_reasons[k] : '';
  1591. const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(labels[k] || k)}', '${esc(k)}')" title="点击定格查看评判理由" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7; font-size: 11px; font-weight: normal; user-select: none;">ⓘ</span>` : '';
  1592. return `<div class="score-row ${m ? 'missing' : ''}"><span>${labels[k] || k}</span><div class="meter" style="--v:${meterV}"><span></span></div><strong style="display: inline-flex; align-items: center;">${valStr}${suffix}${infoIcon}</strong></div>`;
  1593. }).join("");
  1594. const headRight = isFilter ? (g.hint ? `<small>${g.hint}</small>` : '') : `<small>均分 ${fmt(groupAverage(it, g))}</small>`;
  1595. return `<section class="score-group"><div class="score-group-head"><span>${g.title}</span>${headRight}</div><div class="score-group-body">${rows}</div></section>`;
  1596. }
  1597. function curDims() {
  1598. if (st.qi === -1) { if (openCell) { const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti; return { type: TYPES[ti].name, action: ACTIONS[ai].name }; } return {}; }
  1599. const q = DATA.queries[st.qi]; return (q && q.dims) ? q.dims : {};
  1600. }
  1601. function spans(arr, key) { const o = []; let cur = null, s = 0; arr.forEach((x, i) => { if (x[key] !== cur) { if (cur !== null) o.push([cur, s, i - s]); cur = x[key]; s = i; } }); o.push([cur, s, arr.length - s]); return o; }
  1602. const l1sp = spans(ACTIONS, 'l1'), l2sp = spans(ACTIONS, 'l2');
  1603. const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
  1604. function detectLens(q) {
  1605. const text = (q.original_q || (q.forms && q.forms[0] && q.forms[0].query) || "").toLowerCase();
  1606. if (/流程|步骤|教程|方法|教学|SOP|pipeline|工序/.test(text)) return '工序';
  1607. if (/一键|自动|直出|秒出|同款|复刻|效果|技巧|能力/.test(text)) return '能力';
  1608. if (/用什么|软件|工具|推荐|哪个好用|有哪些/.test(text)) return '工具';
  1609. return '工序';
  1610. }
  1611. function findQuery(aName, tName, lens, tool) {
  1612. const matches = DATA.queries.filter(q => {
  1613. if (!q.dims || q.dims.action !== aName || q.dims.type !== tName) return false;
  1614. const qLens = detectLens(q);
  1615. if (qLens !== lens) return false;
  1616. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  1617. if (tool) {
  1618. if (!hasToolConstraint || q.dims.constraint.value !== tool) return false;
  1619. }
  1620. return true;
  1621. });
  1622. if (matches.length > 0) {
  1623. matches.sort((x, y) => {
  1624. if (!tool) {
  1625. const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
  1626. const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
  1627. if (hasX !== hasY) return hasX ? 1 : -1;
  1628. }
  1629. return (y.hits || 0) - (x.hits || 0);
  1630. });
  1631. return matches[0];
  1632. }
  1633. return null;
  1634. }
  1635. function selectQueryByActiveCellAndControls(aName, tName) {
  1636. const activeTool = st.tools[0] || null;
  1637. let match = findQuery(aName, tName, st.lens, activeTool);
  1638. const isProcedureView = st.matrixView === 'procedures';
  1639. const isHitsView = st.matrixView === 'hits';
  1640. const hasContent = q => {
  1641. if (isProcedureView) return getFormProcedureCount(q) > 0;
  1642. if (isHitsView) return getFormReportCount(q) > 0;
  1643. return q.hits > 0;
  1644. };
  1645. if (match) {
  1646. st.qi = DATA.queries.indexOf(match);
  1647. const fi = match.forms.findIndex(f => f.form === st.form);
  1648. st.fi = fi >= 0 ? fi : 0;
  1649. st.selectedAction = aName;
  1650. st.selectedType = tName;
  1651. } else {
  1652. // Fallback: search for any query matching active lens & tool constraint with hits
  1653. const matches = DATA.queries.filter(q => {
  1654. if (!q.dims || !q.dims.action || !q.dims.type) return false;
  1655. const qLens = detectLens(q);
  1656. if (qLens !== st.lens) return false;
  1657. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  1658. if (activeTool) {
  1659. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  1660. } else {
  1661. if (hasToolConstraint) return false;
  1662. }
  1663. return hasContent(q);
  1664. });
  1665. if (matches.length > 0) {
  1666. matches.sort((x, y) => {
  1667. if (!activeTool) {
  1668. const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
  1669. const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
  1670. if (hasX !== hasY) return hasX ? 1 : -1;
  1671. }
  1672. const countX = isProcedureView ? getFormProcedureCount(x) : (isHitsView ? getFormReportCount(x) : x.hits);
  1673. const countY = isProcedureView ? getFormProcedureCount(y) : (isHitsView ? getFormReportCount(y) : y.hits);
  1674. return countY - countX;
  1675. });
  1676. const anyMatch = matches[0];
  1677. st.qi = DATA.queries.indexOf(anyMatch);
  1678. const fi = anyMatch.forms.findIndex(f => f.form === st.form);
  1679. st.fi = fi >= 0 ? fi : 0;
  1680. st.selectedAction = anyMatch.dims.action;
  1681. st.selectedType = anyMatch.dims.type;
  1682. } else {
  1683. st.qi = -1;
  1684. st.fi = 0;
  1685. }
  1686. }
  1687. }
  1688. function getFormReportCount(q) {
  1689. const f = q.forms && q.forms.find(x => x.form === st.form);
  1690. if (!f || !f.results) return 0;
  1691. return f.results.filter(r => !r.anomaly && !isItemDiscarded(r)).length;
  1692. }
  1693. function getFormProcedureCount(q) {
  1694. const f = q.forms && q.forms.find(x => x.form === st.form);
  1695. if (!f || !f.results) return 0;
  1696. return f.results.filter(r => !r.anomaly && !isItemDiscarded(r) && r.procedure_html).length;
  1697. }
  1698. function getFilteredHitsMap() {
  1699. const cm = {};
  1700. const activeTool = st.tools[0] || null;
  1701. const isProcedureView = st.matrixView === 'procedures';
  1702. DATA.queries.forEach((q, i) => {
  1703. const d = q.dims;
  1704. if (!d || !d.type || !d.action) return;
  1705. // Filter by lens
  1706. if (detectLens(q) !== st.lens) return;
  1707. // Filter by active tool constraint
  1708. const hasToolConstraint = d.constraint && d.constraint.kind === "工具类型";
  1709. if (activeTool) {
  1710. if (!hasToolConstraint || d.constraint.value !== activeTool) return;
  1711. } else {
  1712. if (hasToolConstraint) return;
  1713. }
  1714. const k = d.type + '|' + d.action;
  1715. const h = isProcedureView ? getFormProcedureCount(q) : getFormReportCount(q);
  1716. if (!cm[k] || h > cm[k].hits) {
  1717. cm[k] = { i, hits: h };
  1718. }
  1719. });
  1720. return cm;
  1721. }
  1722. function renderMatrix() {
  1723. const viewMode = st.matrixView || 'full';
  1724. const showFull = viewMode === 'full';
  1725. const cm = getFilteredHitsMap();
  1726. const activeActions = showFull ? ACTIONS : ACTIONS.filter(a => TYPES.some(t => {
  1727. const c = cm[t.name + '|' + a.name];
  1728. return c && c.hits > 0;
  1729. }));
  1730. const activeTypes = showFull ? TYPES : TYPES.filter(t => ACTIONS.some(a => {
  1731. const c = cm[t.name + '|' + a.name];
  1732. return c && c.hits > 0;
  1733. }));
  1734. const displayActions = activeActions.length ? activeActions : ACTIONS;
  1735. const displayTypes = activeTypes.length ? activeTypes : TYPES;
  1736. const l1sp = spans(displayActions, 'l1'), l2sp = spans(displayActions, 'l2');
  1737. const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
  1738. let h = '<thead><tr class="l1"><th class="corner" rowspan="3">类型 \ 动作</th>' + l1sp.map(([v, s, c]) => `<th colspan="${c}" class="l1div">${v}</th>`).join('') + '</tr>';
  1739. h += '<tr class="l2">' + l2sp.map(([v, s, c]) => `<th colspan="${c}" class="${l1Start.has(s) ? 'l1div' : 'l2div'}">${v}</th>`).join('') + '</tr>';
  1740. h += '<tr class="leaf">' + displayActions.map((a, i) => `<th data-ai="${ACTIONS.indexOf(a)}" class="${l1Start.has(i) ? 'l1div' : (l2Start.has(i) ? 'l2div' : '')}">${a.name}</th>`).join('') + '</tr></thead><tbody>';
  1741. const typeCategories = ['程序控制类型', '数据复用类型', '内容类型', '知识类型'];
  1742. typeCategories.forEach(l1 => {
  1743. const catTypes = displayTypes.filter(t => t.l1 === l1);
  1744. if (catTypes.length === 0) return;
  1745. h += `<tr class="l1row"><td colspan="${displayActions.length + 1}">${l1}</td></tr>`;
  1746. catTypes.forEach((t) => {
  1747. h += `<tr data-type="${t.name}"><th class="rh" data-ti="${TYPES.indexOf(t)}">${t.name}</th>` + displayActions.map((a) => {
  1748. const ai = ACTIONS.indexOf(a);
  1749. const ti = TYPES.indexOf(t);
  1750. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  1751. const s = cell.tier !== undefined ? cell.tier : cell.s;
  1752. const cls = (s === null || s === undefined) ? 'tNA' : ('t' + s);
  1753. const isSel = (st.selectedAction === a.name && st.selectedType === t.name) ? ' sel' : '';
  1754. return `<td class="cell ${cls}${isSel}" data-ai="${ai}" data-ti="${ti}"></td>`;
  1755. }).join('') + '</tr>';
  1756. });
  1757. });
  1758. document.getElementById('comboMx').innerHTML = h + '</tbody>';
  1759. refresh();
  1760. applyCrosshair();
  1761. }
  1762. // 点中 cell 时浅高亮整行整列(含 row/col 表头),方便定位
  1763. function applyCrosshair() {
  1764. document.querySelectorAll('.rowsel,.colsel').forEach(el => el.classList.remove('rowsel', 'colsel'));
  1765. const ai = st.selectedAction ? ACTIONS.findIndex(a => a.name === st.selectedAction) : -1;
  1766. const ti = st.selectedType ? TYPES.findIndex(t => t.name === st.selectedType) : -1;
  1767. if (ai < 0 && ti < 0) return;
  1768. document.querySelectorAll(`#comboMx [data-ai="${ai}"]`).forEach(el => el.classList.add('colsel'));
  1769. document.querySelectorAll(`#comboMx [data-ti="${ti}"]`).forEach(el => el.classList.add('rowsel'));
  1770. }
  1771. function refresh() {
  1772. const cm = getFilteredHitsMap();
  1773. document.querySelectorAll('tr[data-type]').forEach(tr => {
  1774. const ty = tr.dataset.type;
  1775. tr.querySelectorAll('td.cell').forEach(td => {
  1776. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai];
  1777. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  1778. const s = cell.tier !== undefined ? cell.tier : cell.s;
  1779. const c = cm[ty + '|' + a.name];
  1780. const hits = c ? c.hits : 0;
  1781. td.textContent = genQ(a.name, ty);
  1782. if (hits > 0) {
  1783. const badge = document.createElement('span');
  1784. badge.className = 'hit-badge';
  1785. badge.textContent = hits;
  1786. td.appendChild(badge);
  1787. }
  1788. td.classList.toggle('hide', (s == null ? 0 : s) < (+st.tier));
  1789. const hitsLabel = st.matrixView === 'procedures' ? '有工序' : '命中';
  1790. td.title = `${ty} × ${a.name}\nGemini评分: ${(s === null || s === undefined) ? '未判' : ['无效', '低', '中', '高'][s]}\n当前 form(${st.form}) ${hitsLabel}: ${hits} 篇`;
  1791. });
  1792. });
  1793. }
  1794. const pop = document.getElementById('pop'); let openCell = null;
  1795. function showPop(td, x, y) {
  1796. openCell = td;
  1797. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
  1798. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  1799. const s = cell.tier !== undefined ? cell.tier : cell.s;
  1800. const tn = (s == null) ? '未判' : ['无效', '低', '中', '高'][s], tb = (s == null) ? 'NA' : s;
  1801. const gen = genForms(a.name, t.name, st.lens, toolPrefix(st.lens));
  1802. // Find matching query in database for this cell
  1803. const activeTool = st.tools[0] || null;
  1804. const matches = DATA.queries.filter(q => {
  1805. if (!q.dims || q.dims.action !== a.name || q.dims.type !== t.name) return false;
  1806. const qLens = detectLens(q);
  1807. if (qLens !== st.lens) return false;
  1808. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  1809. if (activeTool) {
  1810. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  1811. } else {
  1812. if (hasToolConstraint) return false;
  1813. }
  1814. return true;
  1815. });
  1816. let dbQuery = null;
  1817. if (matches.length > 0) {
  1818. matches.sort((x, y) => (y.hits || 0) - (x.hits || 0));
  1819. dbQuery = matches[0];
  1820. }
  1821. const dbFormMap = {};
  1822. if (dbQuery) {
  1823. dbQuery.forms.forEach(f => {
  1824. dbFormMap[f.form] = f.query;
  1825. });
  1826. }
  1827. const formKeys = ['A', 'B', 'C'];
  1828. let html = `<span class="close">×</span>
  1829. <div class="pt">
  1830. <span class="path a">${a.l1}›${a.l2}›${a.name}</span>
  1831. <span class="path t">${t.l1}›${t.name}</span>
  1832. <span class="path" style="background: var(--soft-amber); color: var(--amber); font-family: monospace; font-weight: bold; border: 1px solid rgba(184, 121, 24, 0.2);">${dbQuery ? dbQuery.key : '未匹配'}</span>
  1833. <span class="tier tb${tb}">gemini·${tn}</span>
  1834. </div>
  1835. <div class="reason${s === 0 ? ' inv' : ''}">${esc(cell.r || '(模型未给该格评分)')}</div>
  1836. <div class="lbl">系统生成 · 知识类型=${st.lens}${st.tools.length ? ' · 工具类型=' + st.tools.join('/') : ''}</div>
  1837. <ul>` + gen.map(([fName, genQStr], idx) => {
  1838. const formKey = formKeys[idx];
  1839. const actualQ = dbFormMap[formKey];
  1840. const isCurrent = formKey === st.form;
  1841. let content = `<span class="fm">${fName}</span>${esc(genQStr)}`;
  1842. if (actualQ) {
  1843. if (actualQ !== genQStr) {
  1844. content += `<div style="font-size: 13px; margin-top: 6px; color: #047857; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #ecfdf5; border: 1px dashed #34d399; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(4, 120, 87, 0.04);">
  1845. <span style="background: #10b981; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
  1846. <span style="word-break: break-all; font-family: inherit;">${esc(actualQ)}</span>
  1847. </div>`;
  1848. } else {
  1849. content += `<div style="font-size: 13px; margin-top: 6px; color: #374151; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #f9fafb; border: 1px dashed #d1d5db; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);">
  1850. <span style="background: #9ca3af; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
  1851. <span style="font-style: italic; font-family: inherit; color: #6b7280;">(同上)</span>
  1852. </div>`;
  1853. }
  1854. }
  1855. return `<li class="gen${isCurrent ? ' cur' : ''}" style="display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 6px 8px;">${content}</li>`;
  1856. }).join('') + '</ul>';
  1857. pop.innerHTML = html;
  1858. pop.style.display = 'block';
  1859. pop.style.left = Math.max(8, Math.min(x, window.innerWidth - 466)) + 'px';
  1860. pop.style.top = Math.max(8, Math.min(y, window.innerHeight - pop.offsetHeight - 12)) + 'px';
  1861. pop.querySelector('.close').onclick = () => {
  1862. pop.style.display = 'none';
  1863. openCell = null;
  1864. };
  1865. }
  1866. function computeStats() {
  1867. const stat = { "0": 0, "1": 0, "2": 0, "3": 0, "na": 0 };
  1868. if (DATA.matrix) {
  1869. DATA.matrix.forEach(row => row.forEach(cell => {
  1870. const s = cell ? (cell.tier !== undefined ? cell.tier : cell.s) : null;
  1871. if (s === null || s === undefined) stat["na"]++;
  1872. else if (stat[s] !== undefined) stat[s]++;
  1873. }));
  1874. }
  1875. document.getElementById('s3').textContent = stat['3'];
  1876. document.getElementById('s2').textContent = stat['2'];
  1877. document.getElementById('s1').textContent = stat['1'];
  1878. document.getElementById('s0').textContent = stat['0'];
  1879. document.getElementById('sna').textContent = stat['na'];
  1880. }
  1881. function renderFormsChan() {
  1882. const q = st.qi === -1 ? null : DATA.queries[st.qi];
  1883. if (!q) {
  1884. document.getElementById("navC").innerHTML = '';
  1885. return;
  1886. }
  1887. const f = curForm();
  1888. if (!f) {
  1889. document.getElementById("navC").innerHTML = '';
  1890. return;
  1891. }
  1892. const isProcedureView = st.matrixView === 'procedures';
  1893. const resultsFilter = r => {
  1894. if (isProcedureView) {
  1895. return r.procedure_html && r.procedure_html !== "";
  1896. }
  1897. return true;
  1898. };
  1899. const filteredResults = f.results.filter(resultsFilter);
  1900. const chans = ["all", ...f.platforms];
  1901. document.getElementById("navC").innerHTML = chans.map(c => {
  1902. const n = c === "all" ? filteredResults.length : filteredResults.filter(r => r.platformKey === c).length;
  1903. return `<span class="tab ${c === st.channel ? 'on' : ''}" data-c="${c}">${c === "all" ? "全部" : (PLATC[c] || c)} <small>${n}</small></span>`;
  1904. }).join("");
  1905. }
  1906. function renderNav() { renderMatrix(); renderFormsChan(); }
  1907. function renderHead() {
  1908. const f = curForm();
  1909. if (!f) return;
  1910. const q = DATA.queries[st.qi];
  1911. if (q) {
  1912. document.getElementById("lede").innerHTML = `当前选中 Query ID: <span style="font-family: monospace; font-weight: 700; background: var(--soft-amber); color: var(--amber); padding: 2px 8px; border-radius: 4px; font-size: 15px; border: 1px solid rgba(184, 121, 24, 0.2);">${esc(q.key)}</span> · 检索语句: <span style="font-weight: 600; color: var(--ink);">${esc(q.original_q)}</span>`;
  1913. } else {
  1914. document.getElementById("lede").textContent = "";
  1915. }
  1916. let it = f.results;
  1917. if (st.matrixView === 'procedures') {
  1918. it = it.filter(r => r.procedure_html && r.procedure_html !== "");
  1919. }
  1920. if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
  1921. const valid = it.filter(r => !r.anomaly);
  1922. const rep = valid.filter(r => !isItemDiscarded(r)).length;
  1923. const dis = valid.filter(r => isItemDiscarded(r)).length;
  1924. const anom = it.filter(r => r.anomaly).length;
  1925. const avg = (valid.reduce((s, r) => s + r.overall, 0) / (valid.length || 1)).toFixed(1);
  1926. const lab = st.channel === "all" ? "该形式结果数" : (PLATC[st.channel] || st.channel) + " 结果数";
  1927. document.getElementById("stats").innerHTML = [[it.length, lab], [rep, "建议上报"], [avg, "平均综合分 / 5"], [dis, "丢弃"], [anom, "异常"]].map(([n, l]) => `<div class="stat"><strong>${n}</strong><span>${l}</span></div>`).join("");
  1928. }
  1929. // POST /api/reeval —— 后台只对当前 query 的所有 form 文件复评(不重新搜索);
  1930. // server.py 立即返回 {status:'started', pid, log},前端只显示状态、不轮询,刷新页面看新数据。
  1931. function reevalCurrentQuery() {
  1932. if (st.qi === -1 || !DATA.queries[st.qi]) { alert('请先选一个 query 再重评'); return; }
  1933. const q = DATA.queries[st.qi].key;
  1934. if (!confirm(`重评 ${q} 的所有帖子(A/B/C 三种 form)?\n约 1-3 分钟(视帖子数),过程中页面可继续浏览。\n完成后刷新页面看新数据。`)) return;
  1935. const btn = document.getElementById('reevalBtn');
  1936. const oldText = btn.textContent;
  1937. btn.disabled = true; btn.textContent = '♻️ 提交中…';
  1938. fetch('/api/reeval', {
  1939. method: 'POST', headers: { 'Content-Type': 'application/json' },
  1940. body: JSON.stringify({ q }),
  1941. }).then(r => r.json().then(d => ({ ok: r.ok, d }))).then(({ ok, d }) => {
  1942. if (ok && d.status === 'started') {
  1943. btn.textContent = `♻️ 重评中 ${q} (PID ${d.pid}) · 完成后刷新页面`;
  1944. // 不自动恢复 —— 让按钮停留在"重评中"状态直到用户主动刷新;日志见 runs/${q}/_reeval.log
  1945. } else {
  1946. btn.disabled = false; btn.textContent = oldText;
  1947. alert('启动失败:' + (d.error || JSON.stringify(d)));
  1948. }
  1949. }).catch(e => {
  1950. btn.disabled = false; btn.textContent = oldText;
  1951. alert('请求失败:' + e);
  1952. });
  1953. }
  1954. function sortedItems() {
  1955. const f = curForm();
  1956. if (!f) return [];
  1957. let it = f.results.slice();
  1958. if (st.matrixView === 'procedures') {
  1959. it = it.filter(r => r.procedure_html && r.procedure_html !== "");
  1960. }
  1961. if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
  1962. const s = document.getElementById("sort").value;
  1963. const decWeight = r => (r.anomaly ? 2 : (isItemDiscarded(r) ? 1 : 0));
  1964. it.sort((a, b) => {
  1965. const wA = decWeight(a), wB = decWeight(b);
  1966. if (wA !== wB) return wA - wB;
  1967. if (s === "score") return b.overall - a.overall;
  1968. if (s === "date") return (b.date || "").localeCompare(b.date || "");
  1969. if (s === "platform") return (a.platform || "").localeCompare(b.platform || "", "zh-Hans") || b.overall - a.overall;
  1970. return 0;
  1971. });
  1972. return it;
  1973. }
  1974. function renderGrid() {
  1975. VIEW = sortedItems();
  1976. document.getElementById("grid").innerHTML = VIEW.map((it, idx) => {
  1977. const imgs = it.images.length ? it.images.slice(0, 3).map(s => `<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" onerror="this.src='${NOIMG}'">`).join("") : `<img src="${NOIMG}">`;
  1978. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  1979. let bars = "";
  1980. if (isNewSchema) {
  1981. const relAvg = groupAverage(it, scoreGroupsNew[0], true) || 0;
  1982. const qualAvg = getQualityAverage(it) || 0;
  1983. const newBars = [
  1984. { label: "相关性评分 (均分)", v: relAvg },
  1985. { label: "质量评分 (均分)", v: qualAvg }
  1986. ];
  1987. bars = newBars.map(b => {
  1988. const w = b.v * 10;
  1989. return `<span title="${esc(b.label)} ${b.v.toFixed(1)}"><span class="fill" style="width:${w}%; background: #2563eb;"></span></span>`;
  1990. }).join("");
  1991. } else {
  1992. const oldKeys = ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"];
  1993. bars = oldKeys.map(k => {
  1994. const val = it.scores[k] !== undefined ? parseFloat(it.scores[k]) : 0;
  1995. const w = val * 20;
  1996. return `<span title="${esc(commonLabels[k] || k)} ${val}"><span class="fill" style="width:${w}%;"></span></span>`;
  1997. }).join("");
  1998. }
  1999. const isDiscard = isItemDiscarded(it);
  2000. let discardReason = it.reason || '未提供过滤原因';
  2001. if (it.production_relevance !== null && it.production_relevance !== undefined) {
  2002. const input = document.getElementById("relThreshold");
  2003. const threshold = input ? parseFloat(input.value) : (isNewSchema ? 4.0 : 2.0);
  2004. const relVal = parseFloat(it.production_relevance);
  2005. if (!isNaN(relVal) && relVal < threshold) {
  2006. discardReason = `相关性得分 (${relVal}) 低于过滤阈值 (${threshold})`;
  2007. }
  2008. }
  2009. const discardOverlayHtml = isDiscard ? `
  2010. <div class="discard-overlay">
  2011. <div class="discard-badge">Discarded</div>
  2012. <div class="discard-reason">${esc(discardReason)}</div>
  2013. </div>
  2014. ` : '';
  2015. return `<article class="result ${isDiscard ? 'discard' : ''}">
  2016. ${discardOverlayHtml}
  2017. <div class="thumbs">${imgs}</div>
  2018. <div class="body">
  2019. <div class="meta"><span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>${esc(it.date)} · ${esc(it.engagement)}</span></div>
  2020. <h2>${esc(it.title)}</h2>
  2021. <div class="excerpt">${esc(it.text)}</div>
  2022. <div class="tags">${it.tools.slice(0, 4).map(t => `<span class="tag">${esc(t)}</span>`).join("")}</div>
  2023. <div class="scorebar">
  2024. <div class="overall">
  2025. <div>
  2026. <div class="score">${it.anomaly ? '—' : it.overall.toFixed(1)}</div>
  2027. <small>综合分</small>
  2028. </div>
  2029. <div class="decision ${it.anomaly ? '' : it.decision}">${it.anomaly ? '异常' : esc(it.decision)}</div>
  2030. </div>
  2031. <div class="mini-bars ${isNewSchema ? 'new-schema' : ''}">${bars}</div>
  2032. <div class="group-snapshot ${isNewSchema ? 'new-schema' : ''}">${groupSnapshot(it)}</div>
  2033. <div class="actions">
  2034. <button onclick="openDetail(${idx})">查看详情</button>
  2035. <a href="${esc(it.url)}" target="_blank" rel="noreferrer">原链接</a>
  2036. </div>
  2037. </div>
  2038. </div>
  2039. </article>`;
  2040. }).join("") || (st.qi === -1 ? '<p style="color:var(--muted);padding: 20px 0;text-align:center;grid-column: 1 / -1;">该组合暂无数据库扫描结果。</p>' : '<p style="color:var(--muted);grid-column: 1 / -1;">该渠道无结果</p>');
  2041. }
  2042. function switchModalTab(tabName) {
  2043. const detailTab = document.getElementById("tabDetailBtn");
  2044. const procTab = document.getElementById("tabProcedureBtn");
  2045. const detailContent = document.getElementById("modalContentDetail");
  2046. const procContent = document.getElementById("modalContentProcedure");
  2047. if (tabName === 'detail') {
  2048. detailTab.classList.add("active");
  2049. procTab.classList.remove("active");
  2050. detailContent.style.display = "grid";
  2051. procContent.style.display = "none";
  2052. } else {
  2053. detailTab.classList.remove("active");
  2054. procTab.classList.add("active");
  2055. detailContent.style.display = "none";
  2056. procContent.style.display = "block";
  2057. }
  2058. }
  2059. function openDetail(i) {
  2060. const it = VIEW[i];
  2061. detailDialog.dataset.activeIdx = i;
  2062. currentPinnedScoreEl = null;
  2063. document.getElementById("modalMeta").innerHTML = `<span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>Query ID: <b style="font-family: monospace; color: var(--amber); background: var(--soft-amber); border: 1px solid rgba(184, 121, 24, 0.2); padding: 1px 6px; border-radius: 3px; font-size: 11px; margin-right: 8px;">${esc(it.run)}</b></span><span>${esc(it.date)} · ${esc(it.engagement)} · 质量 ${esc(it.grade)} ${esc(it.qscore)}</span>`;
  2064. document.getElementById("modalTitle").textContent = it.title;
  2065. document.getElementById("modalReason").textContent = it.reason;
  2066. document.getElementById("modalText").textContent = it.text || "(无正文)";
  2067. document.getElementById("modalImages").innerHTML = it.images.length ? it.images.map(s => `<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" onerror="this.style.opacity=.3">`).join("") : "<p>搜索详情未返回图片。</p>";
  2068. document.getElementById("modalTags").innerHTML = [...(it.knowledge_type || []).map(t => "类型:" + (KTM[t] || t)), ...(it.found_by || []).map(q => "命中:" + q)].map(t => `<span class="tag">${esc(t)}</span>`).join("");
  2069. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  2070. const maxScore = isNewSchema ? '10' : '5';
  2071. const overallVal = it.anomaly ? '—' : it.overall.toFixed(1);
  2072. const scoreColor = isNewSchema ? '#2563eb' : 'var(--mint)';
  2073. const scoreStrong = document.getElementById("modalOverallScoreVal");
  2074. if (scoreStrong) {
  2075. scoreStrong.style.color = scoreColor;
  2076. scoreStrong.innerHTML = `${overallVal}<span style="font-size: 12px; color: var(--muted); font-weight: 500; margin-left: 2px;">/${maxScore}</span>`;
  2077. }
  2078. if (isNewSchema) {
  2079. document.getElementById("modalScores").innerHTML = renderNewScores(it);
  2080. } else {
  2081. document.getElementById("modalScores").innerHTML = scoreGroupsOld.map(g => renderScoreGroup(it, g)).join("");
  2082. }
  2083. const tabs = document.getElementById("modalTabs");
  2084. const iframe = document.getElementById("procedureIframe");
  2085. if (it.procedure_html) {
  2086. tabs.style.display = "flex";
  2087. iframe.src = "/" + it.procedure_html;
  2088. } else {
  2089. tabs.style.display = "none";
  2090. iframe.src = "about:blank";
  2091. }
  2092. switchModalTab('detail');
  2093. applyFullscreenPref(); // 沿用上次的全屏/窗口偏好
  2094. detailDialog.showModal();
  2095. }
  2096. // 全屏开关: 切 .fullscreen class + 记住偏好 (localStorage), 下次开 case 详情沿用
  2097. function applyFullscreenPref() {
  2098. const on = localStorage.getItem("caseDetailFullscreen") === "1";
  2099. detailDialog.classList.toggle("fullscreen", on);
  2100. const btn = document.getElementById("fullscreenBtn");
  2101. if (btn) btn.textContent = on ? "退出全屏" : "全屏";
  2102. }
  2103. function toggleFullscreen() {
  2104. const on = !detailDialog.classList.contains("fullscreen");
  2105. localStorage.setItem("caseDetailFullscreen", on ? "1" : "0");
  2106. applyFullscreenPref();
  2107. }
  2108. let currentPinnedScoreEl = null;
  2109. function pinScoreReason(el, label, k) {
  2110. const activeIdx = detailDialog.dataset.activeIdx;
  2111. const it = VIEW[activeIdx];
  2112. if (!it || !it.score_reasons) return;
  2113. const reason = it.score_reasons[k] || '';
  2114. let tip = document.getElementById('scoreTip');
  2115. if (!tip) {
  2116. tip = document.createElement('div');
  2117. tip.id = 'scoreTip';
  2118. tip.style.cssText = 'position: fixed; background: #ffffff; border: 1px solid #ded8ce; border-radius: 8px; box-shadow: 0 10px 25px rgba(41, 35, 28, 0.15); padding: 12px 14px; max-width: 320px; z-index: 99999; font-size: 13px; line-height: 1.5; color: #4b5563; word-break: break-all; border-left: 4px solid var(--amber); transition: opacity 0.15s ease;';
  2119. detailDialog.appendChild(tip);
  2120. }
  2121. if (currentPinnedScoreEl === el && tip.style.display === 'block') {
  2122. tip.style.display = 'none';
  2123. el.style.color = '';
  2124. currentPinnedScoreEl = null;
  2125. } else {
  2126. if (currentPinnedScoreEl) {
  2127. currentPinnedScoreEl.style.color = '';
  2128. }
  2129. tip.innerHTML = `<strong style="display:block;margin-bottom:6px;color:#b87918;">【指标判定 - ${label}】</strong>${esc(reason)}`;
  2130. tip.style.display = 'block';
  2131. el.style.color = 'var(--amber)';
  2132. currentPinnedScoreEl = el;
  2133. const elRect = el.getBoundingClientRect();
  2134. const tipWidth = tip.offsetWidth || 280;
  2135. const tipHeight = tip.offsetHeight || 80;
  2136. let left = elRect.left + elRect.width / 2 - tipWidth / 2;
  2137. let top = elRect.bottom + 8;
  2138. if (left < 10) left = 10;
  2139. if (left + tipWidth > window.innerWidth - 10) {
  2140. left = window.innerWidth - tipWidth - 10;
  2141. }
  2142. if (top + tipHeight > window.innerHeight - 10) {
  2143. top = elRect.top - tipHeight - 8;
  2144. }
  2145. tip.style.left = left + 'px';
  2146. tip.style.top = top + 'px';
  2147. }
  2148. }
  2149. // Automatically close tooltip when modal is closed
  2150. document.getElementById('detailDialog').addEventListener('close', () => {
  2151. const tip = document.getElementById('scoreTip');
  2152. if (tip) {
  2153. tip.style.display = 'none';
  2154. }
  2155. if (currentPinnedScoreEl) {
  2156. currentPinnedScoreEl.style.color = '';
  2157. currentPinnedScoreEl = null;
  2158. }
  2159. });
  2160. // Close tooltip when dialog scrolls
  2161. document.getElementById('detailDialog').addEventListener('scroll', () => {
  2162. const tip = document.getElementById('scoreTip');
  2163. if (tip) {
  2164. tip.style.display = 'none';
  2165. }
  2166. if (currentPinnedScoreEl) {
  2167. currentPinnedScoreEl.style.color = '';
  2168. currentPinnedScoreEl = null;
  2169. }
  2170. }, true);
  2171. const KTM = { procedure: "工序", step: "步骤", tool: "工具" };
  2172. function rerender(mxClick) {
  2173. updateThresholdLimits();
  2174. if (st.qi === -1) {
  2175. document.getElementById("stats").innerHTML = "";
  2176. document.getElementById("lede").textContent = "未找到对应的数据库 Query,请尝试切换筛选组合。";
  2177. renderFormsChan();
  2178. renderGrid();
  2179. if (!mxClick) {
  2180. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  2181. }
  2182. return;
  2183. }
  2184. if (st.qi >= DATA.queries.length) st.qi = 0;
  2185. if (st.fi >= DATA.queries[st.qi].forms.length) st.fi = 0;
  2186. if (!mxClick) {
  2187. renderMatrix();
  2188. }
  2189. renderFormsChan();
  2190. renderHead();
  2191. renderGrid();
  2192. }
  2193. function setMatrixView(mode) {
  2194. st.matrixView = mode;
  2195. document.querySelectorAll('.g-mxview .btn').forEach(btn => {
  2196. btn.classList.remove('on');
  2197. });
  2198. if (mode === 'full') document.getElementById('btnMxFull').classList.add('on');
  2199. else if (mode === 'hits') document.getElementById('btnMxHits').classList.add('on');
  2200. else if (mode === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
  2201. if (st.selectedAction && st.selectedType) {
  2202. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2203. }
  2204. renderMatrix();
  2205. if (openCell) {
  2206. const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
  2207. const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
  2208. if (newCell) {
  2209. showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
  2210. }
  2211. }
  2212. rerender(true);
  2213. }
  2214. function loadData(keep) {
  2215. fetch("/api/data").then(r => r.json()).then(d => {
  2216. DATA = d;
  2217. if (!keep) {
  2218. st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full', selectedAction: null, selectedType: null };
  2219. if (DATA.queries.length > 0) {
  2220. const firstQ = DATA.queries[0];
  2221. if (firstQ.dims) {
  2222. st.selectedAction = firstQ.dims.action;
  2223. st.selectedType = firstQ.dims.type;
  2224. st.lens = detectLens(firstQ);
  2225. }
  2226. }
  2227. }
  2228. // Dynamically populate tool type buttons
  2229. document.getElementById('toolBtns').innerHTML = TOOL_TYPES.map(t => `<button class="btn" data-k="tool" data-v="${t}">${t}</button>`).join('');
  2230. // Synchronize buttons states to st
  2231. document.querySelectorAll('.ctl .btn').forEach(btn => {
  2232. const k = btn.dataset.k, v = btn.dataset.v;
  2233. if (MULTI[k]) {
  2234. const ak = MULTI[k];
  2235. btn.classList.toggle('on', v === '' ? st[ak].length === 0 : st[ak].includes(v));
  2236. } else {
  2237. btn.classList.toggle('on', st[k] === v);
  2238. }
  2239. });
  2240. document.querySelectorAll('.g-mxview .btn').forEach(btn => {
  2241. btn.classList.remove('on');
  2242. });
  2243. const mv = st.matrixView || 'full';
  2244. if (mv === 'full') document.getElementById('btnMxFull').classList.add('on');
  2245. else if (mv === 'hits') document.getElementById('btnMxHits').classList.add('on');
  2246. else if (mv === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
  2247. computeStats();
  2248. if (st.selectedAction && st.selectedType) {
  2249. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2250. }
  2251. rerender();
  2252. });
  2253. }
  2254. // Matrix click listener (links matrix select to database select + opens pop-up)
  2255. document.getElementById("comboMx").addEventListener("click", e => {
  2256. const td = e.target.closest("td.cell");
  2257. if (!td) return;
  2258. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
  2259. st.selectedAction = a.name;
  2260. st.selectedType = t.name;
  2261. showPop(td, e.clientX, e.clientY);
  2262. // Select the query
  2263. selectQueryByActiveCellAndControls(a.name, t.name);
  2264. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  2265. td.classList.add('sel');
  2266. applyCrosshair();
  2267. rerender(true); // mxClick = true, updates results without redrawing full table
  2268. });
  2269. // Header controls event delegation
  2270. const MULTI = { tool: 'tools' };
  2271. document.querySelector('.ctl').addEventListener('click', e => {
  2272. const b = e.target.closest('.btn');
  2273. if (!b) return;
  2274. const k = b.dataset.k, v = b.dataset.v, grp = document.querySelectorAll(`.ctl .btn[data-k="${k}"]`);
  2275. if (MULTI[k]) {
  2276. const ak = MULTI[k];
  2277. if (v === '') {
  2278. st[ak] = [];
  2279. } else {
  2280. if (st[ak].includes(v)) {
  2281. st[ak] = [];
  2282. } else {
  2283. st[ak] = [v];
  2284. }
  2285. }
  2286. grp.forEach(x => {
  2287. const xv = x.dataset.v;
  2288. x.classList.toggle('on', xv === '' ? st[ak].length === 0 : st[ak].includes(xv));
  2289. });
  2290. } else {
  2291. grp.forEach(x => x.classList.remove('on'));
  2292. b.classList.add('on');
  2293. st[k] = v;
  2294. }
  2295. if (st.selectedAction && st.selectedType) {
  2296. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2297. }
  2298. renderMatrix();
  2299. if (openCell) {
  2300. const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
  2301. const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
  2302. if (newCell) {
  2303. showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
  2304. }
  2305. }
  2306. rerender(true);
  2307. });
  2308. // Close popups on clicking outside
  2309. document.addEventListener('click', e => {
  2310. if (!e.target.closest('td.cell') && !e.target.closest('.pop') && !e.target.closest('.btn')) {
  2311. pop.style.display = 'none';
  2312. openCell = null;
  2313. }
  2314. const tip = document.getElementById('scoreTip');
  2315. if (tip && !e.target.closest('.info-icon') && !e.target.closest('#scoreTip')) {
  2316. tip.style.display = 'none';
  2317. if (currentPinnedScoreEl) {
  2318. currentPinnedScoreEl.style.color = '';
  2319. currentPinnedScoreEl = null;
  2320. }
  2321. }
  2322. });
  2323. document.getElementById("navC").addEventListener("click", e => { const t = e.target.closest(".tab"); if (!t) return; st.channel = t.dataset.c; renderNav(); renderHead(); renderGrid(); });
  2324. document.getElementById("sort").addEventListener("change", renderGrid);
  2325. loadData();
  2326. </script>
  2327. </body>
  2328. </html>