index.html 137 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392
  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. dialog::backdrop {
  370. background: rgba(38, 33, 27, .42);
  371. }
  372. .modal-head {
  373. position: sticky;
  374. top: 0;
  375. background: #fff;
  376. border-bottom: 1px solid var(--line);
  377. padding: 16px;
  378. z-index: 2;
  379. display: flex;
  380. justify-content: space-between;
  381. gap: 14px;
  382. align-items: start;
  383. }
  384. .modal-head h3 {
  385. margin: 0;
  386. font-size: 20px;
  387. line-height: 1.25;
  388. }
  389. .modal-content {
  390. display: grid;
  391. grid-template-columns: 1.1fr .9fr;
  392. gap: 18px;
  393. padding: 16px;
  394. }
  395. .modal-content > section,
  396. .modal-content > aside {
  397. min-width: 0;
  398. }
  399. .section-title {
  400. margin: 18px 0 8px;
  401. font-weight: 800;
  402. }
  403. .raw {
  404. white-space: pre-wrap;
  405. background: #faf7f1;
  406. border: 1px solid var(--line);
  407. border-radius: 8px;
  408. padding: 12px;
  409. max-height: 330px;
  410. overflow: auto;
  411. color: #3d3831;
  412. font-size: 13px;
  413. }
  414. .images {
  415. display: grid;
  416. grid-template-columns: repeat(2, minmax(0, 1fr));
  417. gap: 8px;
  418. }
  419. .images img {
  420. width: 100%;
  421. max-height: 260px;
  422. object-fit: contain;
  423. border: 1px solid var(--line);
  424. border-radius: 8px;
  425. background: #f1ece4;
  426. }
  427. .scores {
  428. display: grid;
  429. gap: 12px;
  430. }
  431. .score-group {
  432. border: 1px solid var(--line);
  433. border-radius: 8px;
  434. background: #fff;
  435. overflow: hidden;
  436. }
  437. .score-group-head {
  438. display: flex;
  439. justify-content: space-between;
  440. gap: 10px;
  441. align-items: center;
  442. padding: 10px 11px;
  443. background: #faf7f1;
  444. border-bottom: 1px solid var(--line);
  445. font-size: 13px;
  446. font-weight: 800;
  447. }
  448. .score-group-head small {
  449. color: var(--muted);
  450. font-weight: 700;
  451. white-space: nowrap;
  452. }
  453. .score-group-body {
  454. display: grid;
  455. gap: 8px;
  456. padding: 10px;
  457. }
  458. .score-row {
  459. display: grid;
  460. grid-template-columns: 128px 1fr 34px;
  461. gap: 10px;
  462. align-items: center;
  463. font-size: 13px;
  464. }
  465. .score-row.missing {
  466. color: #a39b91;
  467. }
  468. .score-row.missing .meter span {
  469. display: none;
  470. }
  471. .meter {
  472. height: 9px;
  473. border-radius: 999px;
  474. background: #eee7dc;
  475. overflow: hidden;
  476. }
  477. .meter span {
  478. display: block;
  479. height: 100%;
  480. width: calc(var(--v) * 20%);
  481. background: var(--rose);
  482. }
  483. .rubric-note {
  484. background: var(--soft-cyan);
  485. border-left: 4px solid var(--cyan);
  486. padding: 10px 12px;
  487. color: #254c5d;
  488. border-radius: 4px;
  489. font-size: 13px;
  490. }
  491. @media (max-width: 980px) {
  492. .grid {
  493. grid-template-columns: repeat(2, minmax(0, 1fr));
  494. }
  495. .stats {
  496. grid-template-columns: repeat(2, minmax(0, 1fr));
  497. }
  498. .modal-content {
  499. grid-template-columns: 1fr;
  500. }
  501. }
  502. @media (max-width: 640px) {
  503. header,
  504. main {
  505. padding-left: 16px;
  506. padding-right: 16px;
  507. }
  508. .grid,
  509. .stats {
  510. grid-template-columns: 1fr;
  511. }
  512. .toolbar {
  513. align-items: stretch;
  514. }
  515. select {
  516. width: 100%;
  517. }
  518. .result {
  519. min-height: auto;
  520. }
  521. .group-snapshot {
  522. grid-template-columns: repeat(2, minmax(0, 1fr));
  523. }
  524. }
  525. /* Extra styles for interactive navigation & matrix */
  526. .stats {
  527. display: grid;
  528. grid-template-columns: repeat(5, 1fr);
  529. gap: 12px;
  530. }
  531. .stats .stat {
  532. min-width: 0;
  533. }
  534. .p-zhihu {
  535. background: #2a6f8f;
  536. }
  537. .p-x {
  538. background: #2a6f8f;
  539. }
  540. .p-bili {
  541. background: #b24b63;
  542. }
  543. .p-douyin {
  544. background: #24211d;
  545. }
  546. .p-sph {
  547. background: #07c160;
  548. }
  549. .p-youtube {
  550. background: #c4302b;
  551. }
  552. .p-github {
  553. background: #24292e;
  554. }
  555. .p-toutiao {
  556. background: #f04142;
  557. }
  558. .p-weibo {
  559. background: #e6162d;
  560. }
  561. .nav {
  562. display: flex;
  563. flex-direction: column;
  564. gap: 8px;
  565. margin-bottom: 14px;
  566. }
  567. .navrow {
  568. display: flex;
  569. gap: 8px;
  570. align-items: center;
  571. flex-wrap: wrap;
  572. }
  573. .navlab {
  574. width: 54px;
  575. flex: 0 0 54px;
  576. color: var(--muted);
  577. font-size: 12px;
  578. font-weight: 700;
  579. text-transform: uppercase;
  580. }
  581. .navrow .tab {
  582. border: 1px solid var(--line);
  583. background: #fff;
  584. color: var(--ink);
  585. border-radius: 999px;
  586. padding: 6px 14px;
  587. font-size: 13px;
  588. cursor: pointer;
  589. }
  590. .navrow .tab.on {
  591. background: var(--ink);
  592. color: #fff;
  593. border-color: var(--ink);
  594. }
  595. .navrow .tab .q {
  596. font-family: ui-monospace, Menlo, monospace;
  597. }
  598. .navrow .tab small {
  599. color: var(--muted);
  600. margin-left: 4px;
  601. }
  602. .navrow .tab.on small {
  603. color: #cfd8dc;
  604. }
  605. #navQ .qtab {
  606. max-width: 100%;
  607. white-space: normal;
  608. text-align: left;
  609. line-height: 1.4;
  610. }
  611. .navrow .tab {
  612. white-space: normal;
  613. }
  614. #refresh {
  615. cursor: pointer;
  616. }
  617. /* 1:1 Morandi Matrix Replicated Style Rules */
  618. .btn {
  619. padding: 3px 10px;
  620. border-radius: 14px;
  621. border: 1px solid #d1d5db;
  622. background: #fff;
  623. cursor: pointer;
  624. font-size: .74rem;
  625. color: #444;
  626. }
  627. .g-form .btn.on {
  628. background: #4f46e5;
  629. border-color: #4f46e5;
  630. color: #fff;
  631. }
  632. .g-lens .btn.on {
  633. background: #be185d;
  634. border-color: #be185d;
  635. color: #fff;
  636. }
  637. .g-mod .btn.on {
  638. background: #2e7d32;
  639. border-color: #2e7d32;
  640. color: #fff;
  641. }
  642. .g-tool .btn.on {
  643. background: #c2410c;
  644. border-color: #c2410c;
  645. color: #fff;
  646. }
  647. .g-tier .btn.on {
  648. background: #374151;
  649. border-color: #374151;
  650. color: #fff;
  651. }
  652. .g-mxview .btn.on {
  653. background: var(--mint);
  654. border-color: var(--mint);
  655. color: #fff;
  656. }
  657. .mxwrap {
  658. max-height: 55vh;
  659. overflow: auto;
  660. border: 1px solid var(--line);
  661. border-radius: 0 0 8px 8px;
  662. margin-bottom: 12px;
  663. background: #fff;
  664. position: relative;
  665. }
  666. table#comboMx {
  667. border-collapse: separate;
  668. border-spacing: 0;
  669. background: #fff;
  670. font-size: .65rem;
  671. width: 100%;
  672. }
  673. table#comboMx th,
  674. table#comboMx td {
  675. border-right: 1px solid #f0f1f5;
  676. border-bottom: 1px solid #f0f1f5;
  677. text-align: center;
  678. }
  679. table#comboMx thead th {
  680. position: sticky;
  681. background: #fff;
  682. }
  683. table#comboMx thead tr.l1 th {
  684. top: 0;
  685. z-index: 11;
  686. background: #eef2ff;
  687. color: #4338ca;
  688. font-weight: 700;
  689. font-size: .72rem;
  690. height: 20px;
  691. border-bottom: 1px solid #c7d2fe;
  692. }
  693. table#comboMx thead tr.l2 th {
  694. top: 20px;
  695. z-index: 11;
  696. background: #f5f7ff;
  697. color: #6366f1;
  698. font-weight: 600;
  699. font-size: .66rem;
  700. height: 18px;
  701. border-bottom: 1px solid #e0e7ff;
  702. }
  703. table#comboMx thead tr.leaf th {
  704. top: 38px;
  705. z-index: 9;
  706. background: #fff;
  707. color: #1f2937;
  708. font-size: .66rem;
  709. min-width: 120px;
  710. max-width: 120px;
  711. height: 24px;
  712. padding: 3px;
  713. }
  714. table#comboMx thead .corner {
  715. left: 0;
  716. top: 0;
  717. z-index: 13;
  718. background: #fff;
  719. min-width: 96px;
  720. }
  721. .l1div {
  722. border-left: 3px solid #818cf8 !important;
  723. }
  724. .l2div {
  725. border-left: 1.5px solid #cbd5e8 !important;
  726. }
  727. table#comboMx tbody th.rh {
  728. position: sticky;
  729. left: 0;
  730. z-index: 8;
  731. background: #fffaf0;
  732. color: #92580c;
  733. text-align: left;
  734. padding: 4px 8px;
  735. font-weight: 600;
  736. white-space: nowrap;
  737. min-width: 96px;
  738. border-right: 1px solid #e6e8ef;
  739. }
  740. table#comboMx tbody tr.l1row td {
  741. background: #eef2ff;
  742. color: #4338ca;
  743. font-weight: 700;
  744. font-size: .7rem;
  745. text-align: left;
  746. padding: 3px 8px;
  747. position: sticky;
  748. left: 0;
  749. }
  750. td.cell {
  751. min-width: 120px;
  752. max-width: 120px;
  753. height: 30px;
  754. padding: 2px 5px;
  755. cursor: pointer;
  756. font-family: ui-monospace, Menlo, monospace;
  757. font-size: .65rem;
  758. color: #1f2937;
  759. text-align: left;
  760. line-height: 1.25;
  761. border-left: 4px solid transparent;
  762. overflow: hidden;
  763. position: relative;
  764. }
  765. td.cell:hover {
  766. box-shadow: inset 0 0 0 2px #f59e0b;
  767. }
  768. td.cell.t0 {
  769. border-left-color: #e5e7eb;
  770. background: #fbfbfc;
  771. color: #aeb3bd;
  772. }
  773. td.cell.t1 {
  774. border-left-color: #bbf7d0;
  775. }
  776. td.cell.t2 {
  777. border-left-color: #4ade80;
  778. background: #f3fdf6;
  779. }
  780. td.cell.t3 {
  781. border-left-color: #15803d;
  782. background: #ecfdf3;
  783. }
  784. td.cell.tNA {
  785. border-left-color: #fcd34d;
  786. background: repeating-linear-gradient(45deg, #fff, #fff 4px, #fef9ec 4px, #fef9ec 8px);
  787. color: #b6bac4;
  788. }
  789. td.cell.gq-cell {
  790. color: #15803d;
  791. font-weight: 600;
  792. }
  793. td.cell.rowdim {
  794. opacity: .26;
  795. }
  796. td.cell.hide {
  797. visibility: hidden;
  798. }
  799. td.cell.sel {
  800. outline: 2px solid var(--ink);
  801. outline-offset: -2px;
  802. font-weight: bold;
  803. }
  804. td.cell.rowsel,
  805. td.cell.colsel {
  806. box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .12);
  807. }
  808. td.cell.rowsel.colsel {
  809. box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .22);
  810. }
  811. th.rh.rowsel {
  812. background: rgba(255, 193, 7, .22);
  813. color: var(--ink);
  814. font-weight: 700;
  815. }
  816. thead th.colsel {
  817. background: rgba(255, 193, 7, .22);
  818. color: var(--ink);
  819. font-weight: 700;
  820. }
  821. /* Database hit badge inside matrix cells */
  822. td.cell .hit-badge {
  823. position: absolute;
  824. right: 2px;
  825. top: 2px;
  826. background: #10b981;
  827. color: #fff;
  828. font-size: 9px;
  829. font-weight: bold;
  830. border-radius: 3px;
  831. padding: 0 3px;
  832. line-height: 1.2;
  833. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  834. }
  835. /* Pop-up floating panel styling */
  836. .pop {
  837. position: fixed;
  838. background: #fff;
  839. border: 1px solid #d1d5db;
  840. border-radius: 9px;
  841. box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
  842. padding: 13px 15px;
  843. max-width: 450px;
  844. z-index: 200;
  845. display: none;
  846. font-size: .82rem;
  847. line-height: 1.5;
  848. }
  849. .pop .pt {
  850. font-weight: 700;
  851. color: #1f2937;
  852. margin-bottom: 7px;
  853. display: flex;
  854. gap: 6px;
  855. align-items: center;
  856. flex-wrap: wrap;
  857. padding-right: 18px;
  858. }
  859. .pop .path {
  860. font-size: .66rem;
  861. padding: 1px 6px;
  862. border-radius: 3px;
  863. }
  864. .pop .path.a {
  865. background: #eef2ff;
  866. color: #4338ca;
  867. }
  868. .pop .path.t {
  869. background: #fef3c7;
  870. color: #92400e;
  871. }
  872. .pop .tier {
  873. font-size: .64rem;
  874. padding: 1px 7px;
  875. border-radius: 3px;
  876. font-weight: 600;
  877. }
  878. .tb0 {
  879. background: #e5e7eb;
  880. color: #6b7280;
  881. }
  882. .tb1 {
  883. background: #dcfce7;
  884. color: #166534;
  885. }
  886. .tb2 {
  887. background: #bbf7d0;
  888. color: #14532d;
  889. }
  890. .tb3 {
  891. background: #15803d;
  892. color: #fff;
  893. }
  894. .tbNA {
  895. background: #fef3c7;
  896. color: #92400e;
  897. }
  898. .pop .reason {
  899. background: #fffbeb;
  900. border-left: 3px solid #f59e0b;
  901. padding: 7px 11px;
  902. border-radius: 3px;
  903. color: #78350f;
  904. margin: 7px 0;
  905. font-size: .8rem;
  906. }
  907. .pop .reason.inv {
  908. background: #fef2f2;
  909. border-left-color: #ef4444;
  910. color: #991b1b;
  911. }
  912. .pop .gq {
  913. background: #ecfdf3;
  914. border: 1px solid #a7f3d0;
  915. border-radius: 6px;
  916. padding: 7px 11px;
  917. margin: 7px 0;
  918. font-family: ui-monospace, Menlo, monospace;
  919. font-size: .84rem;
  920. color: #15803d;
  921. }
  922. .pop .gq .t {
  923. font-family: inherit;
  924. font-size: .64rem;
  925. color: #6b7280;
  926. display: block;
  927. margin-bottom: 2px;
  928. }
  929. .pop .small {
  930. font-size: .7rem;
  931. color: #9aa0ad;
  932. margin: 4px 0;
  933. }
  934. .pop .lbl {
  935. font-size: .66rem;
  936. color: #6b7280;
  937. font-weight: 600;
  938. margin: 9px 0 3px;
  939. }
  940. .pop ul {
  941. margin: 0;
  942. padding: 0;
  943. list-style: none;
  944. }
  945. .pop li {
  946. padding: 3px 8px;
  947. background: #f9fafb;
  948. border-radius: 4px;
  949. margin-bottom: 3px;
  950. font-family: ui-monospace, Menlo, monospace;
  951. font-size: .78rem;
  952. color: #1f2937;
  953. }
  954. .pop li.gen {
  955. background: #eef0ff;
  956. color: #3730a3;
  957. }
  958. .pop li.cur {
  959. box-shadow: inset 0 0 0 2px #818cf8;
  960. }
  961. .pop li .fm {
  962. font-size: .62rem;
  963. color: #4f46e5;
  964. margin-right: 6px;
  965. }
  966. .pop .note {
  967. font-size: .7rem;
  968. color: #2e7d32;
  969. margin-top: 6px;
  970. }
  971. .pop .close {
  972. position: absolute;
  973. top: 6px;
  974. right: 9px;
  975. cursor: pointer;
  976. color: #9ca3af;
  977. font-size: 1.15rem;
  978. }
  979. .fac {
  980. border: 1px solid var(--line);
  981. background: #fff;
  982. border-radius: 999px;
  983. padding: 4px 11px;
  984. font-size: 12px;
  985. cursor: pointer;
  986. color: var(--ink);
  987. }
  988. .fac.on {
  989. background: var(--mint);
  990. color: #fff;
  991. border-color: var(--mint);
  992. }
  993. .fac small {
  994. opacity: .65;
  995. margin-left: 3px;
  996. }
  997. #navQ .qtab .hit {
  998. display: inline-block;
  999. margin-left: 7px;
  1000. background: var(--soft-mint);
  1001. color: var(--mint);
  1002. border-radius: 999px;
  1003. padding: 0 8px;
  1004. font-size: 11px;
  1005. font-weight: 700;
  1006. }
  1007. #navQ .qtab.on .hit {
  1008. background: rgba(255, 255, 255, .22);
  1009. color: #fff;
  1010. }
  1011. .modal-tabs {
  1012. display: flex;
  1013. gap: 4px;
  1014. padding: 0 16px;
  1015. border-bottom: 1px solid var(--line);
  1016. background: #faf7f1;
  1017. }
  1018. .modal-tab {
  1019. background: transparent;
  1020. border: none;
  1021. border-bottom: 3px solid transparent;
  1022. border-radius: 0;
  1023. padding: 10px 16px;
  1024. font-size: 14px;
  1025. font-weight: 600;
  1026. color: var(--muted);
  1027. cursor: pointer;
  1028. transition: all 0.2s ease;
  1029. }
  1030. .modal-tab:hover {
  1031. color: var(--ink);
  1032. background: rgba(0, 0, 0, 0.02);
  1033. }
  1034. .modal-tab.active {
  1035. color: var(--mint);
  1036. border-bottom-color: var(--mint);
  1037. }
  1038. /* 新版评估分数卡片可视化样式 */
  1039. .sc-card {
  1040. background: #fff;
  1041. border: 1px solid var(--line);
  1042. border-radius: 12px;
  1043. padding: 16px 20px;
  1044. margin-bottom: 16px;
  1045. box-shadow: 0 4px 15px rgba(0,0,0,0.02);
  1046. }
  1047. .sc-card-head {
  1048. display: flex;
  1049. justify-content: space-between;
  1050. align-items: center;
  1051. margin-bottom: 14px;
  1052. border-bottom: 1px solid #f3f0ea;
  1053. padding-bottom: 10px;
  1054. }
  1055. .sc-card-head .title {
  1056. font-size: 16px;
  1057. font-weight: 700;
  1058. display: flex;
  1059. align-items: center;
  1060. gap: 8px;
  1061. }
  1062. .sc-card-head .badge {
  1063. background: #eef2ff;
  1064. color: #2563eb;
  1065. font-size: 11px;
  1066. padding: 2px 6px;
  1067. border-radius: 4px;
  1068. font-weight: 700;
  1069. }
  1070. .sc-card-head .avg-score {
  1071. font-size: 14px;
  1072. color: var(--muted);
  1073. font-weight: 600;
  1074. }
  1075. .sc-card-head .avg-score strong {
  1076. font-size: 26px;
  1077. color: #2563eb;
  1078. font-weight: 800;
  1079. margin-left: 6px;
  1080. }
  1081. .sc-sub-header {
  1082. font-size: 12px;
  1083. color: var(--muted);
  1084. font-weight: 700;
  1085. margin: 14px 0 8px;
  1086. border-bottom: 1px dashed #f0ebd8;
  1087. padding-bottom: 4px;
  1088. text-transform: uppercase;
  1089. letter-spacing: 0.5px;
  1090. }
  1091. .sc-row {
  1092. display: flex;
  1093. justify-content: space-between;
  1094. align-items: center;
  1095. padding: 6px 0;
  1096. font-size: 13.5px;
  1097. gap: 10px;
  1098. }
  1099. .sc-row .label {
  1100. color: var(--ink);
  1101. font-weight: 500;
  1102. flex: 1;
  1103. min-width: 100px;
  1104. word-break: break-all;
  1105. }
  1106. .sc-row .bar-wrap {
  1107. display: flex;
  1108. align-items: center;
  1109. gap: 10px;
  1110. width: 170px;
  1111. flex-shrink: 0;
  1112. }
  1113. .sc-row .bar {
  1114. height: 6px;
  1115. background: #eee7dc;
  1116. border-radius: 999px;
  1117. flex: 1;
  1118. overflow: hidden;
  1119. }
  1120. .sc-row .bar-fill {
  1121. height: 100%;
  1122. background: #2563eb;
  1123. border-radius: 999px;
  1124. width: calc(var(--v) * 10%);
  1125. }
  1126. .sc-row .value {
  1127. font-weight: 700;
  1128. font-size: 13.5px;
  1129. width: 20px;
  1130. text-align: right;
  1131. }
  1132. .sc-row .info-icon {
  1133. cursor: pointer;
  1134. color: #9ca3af;
  1135. transition: color 0.15s ease;
  1136. font-size: 13px;
  1137. user-select: none;
  1138. }
  1139. .sc-row .info-icon:hover {
  1140. color: #3b82f6;
  1141. }
  1142. #reevalBtn {
  1143. background: #faf7f1;
  1144. color: var(--amber);
  1145. border-color: rgba(184, 121, 24, 0.3);
  1146. height: 38px;
  1147. padding: 0 16px;
  1148. font-size: 13px;
  1149. font-weight: 600;
  1150. border-radius: 8px;
  1151. box-shadow: var(--shadow);
  1152. }
  1153. #reevalBtn:hover {
  1154. background: var(--soft-amber);
  1155. color: var(--amber);
  1156. border-color: var(--amber);
  1157. }
  1158. #editSpecBtn {
  1159. background: #f0f7f6;
  1160. color: var(--cyan);
  1161. border-color: rgba(42, 111, 143, 0.3);
  1162. height: 38px;
  1163. padding: 0 16px;
  1164. font-size: 13px;
  1165. font-weight: 600;
  1166. border-radius: 8px;
  1167. box-shadow: var(--shadow);
  1168. }
  1169. #editSpecBtn:hover {
  1170. background: var(--soft-cyan);
  1171. color: var(--cyan);
  1172. border-color: var(--cyan);
  1173. }
  1174. </style>
  1175. </head>
  1176. <body>
  1177. <header>
  1178. <div class="wrap">
  1179. <div class="eyebrow">Content Search · 固定 query · 同义扩展 + 合并去重 · query → 渠道</div>
  1180. <h1>固定 Query 搜索评估 · 案例总览</h1>
  1181. <p class="lede" id="lede" style="margin:0">加载中…</p>
  1182. <div class="stats" id="stats"></div>
  1183. </div>
  1184. </header>
  1185. <main>
  1186. <div class="wrap">
  1187. <div class="nav">
  1188. <!-- fixed_query_eval:隐藏原正交矩阵导航,改用固定 query 选择条 #navQ。
  1189. 矩阵 DOM 保留(display:none)以免 loadData 里的 getElementById 取空崩脚本。 -->
  1190. <style>#mxRow, .mx-header, .mxwrap, #pop { display: none !important; }</style>
  1191. <div class="navrow" id="navQRow" style="margin-bottom:4px;">
  1192. <span class="navlab">Query</span>
  1193. <div id="navQ" style="display:flex;gap:8px;flex-wrap:wrap;flex:1"></div>
  1194. <button id="toolBatchBtn" onclick="openToolBatchModal()" style="margin-right:8px">🔧 工具解构</button>
  1195. <button id="refresh2" onclick="loadData(true)">↻ 刷新 runs</button>
  1196. </div>
  1197. <div class="navrow" id="mxRow" style="margin-bottom:4px;">
  1198. <span class="navlab">组合矩阵</span>
  1199. <span style="color:var(--muted);font-size:12px;flex:1">行=类型 列=动作 格子色=评分,角标=帖子数/工序数。点击选组合且弹出详情。</span>
  1200. <span class="g-mxview" style="display:inline-flex;gap:4px;margin-right:12px;align-items:center;">
  1201. <button class="btn on" id="btnMxFull" onclick="setMatrixView('full')">完整矩阵</button>
  1202. <button class="btn" id="btnMxHits" onclick="setMatrixView('hits')">只看帖子命中</button>
  1203. <button class="btn" id="btnMxProcedures" onclick="setMatrixView('procedures')">只看工序</button>
  1204. </span>
  1205. <button id="refresh" onclick="loadData(true)">↻ 刷新 runs</button>
  1206. </div>
  1207. <div class="mx-header"
  1208. style="background:#fff; border: 1px solid var(--line); border-radius: 8px 8px 0 0; padding:12px 16px 8px; border-bottom: none;">
  1209. <h4
  1210. style="margin:0 0 4px; font-size:13px; color:#1f2937; display:flex; justify-content:space-between; align-items:center; font-weight:700;">
  1211. <span>动作 × 类型 · 组合矩阵 <span style="font-weight:400;font-size:11px;color:#9aa0ad">基于 <b
  1212. id="gm">gemini-3.1-flash-lite</b></span></span>
  1213. <span class="legend" style="display:flex;gap:9px;align-items:center;color:#6b7280;font-size:11px;">
  1214. <span><span class="sw"
  1215. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#15803d;vertical-align:middle;margin-right:3px;"></span>高
  1216. · <b id="s3" style="color:#15803d;">0</b></span>
  1217. <span><span class="sw"
  1218. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#4ade80;vertical-align:middle;margin-right:3px;"></span>中
  1219. · <b id="s2" style="color:#4ade80;">0</b></span>
  1220. <span><span class="sw"
  1221. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#bbf7d0;vertical-align:middle;margin-right:3px;"></span>低
  1222. · <b id="s1" style="color:#10b981;">0</b></span>
  1223. <span><span class="sw"
  1224. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#e5e7eb;vertical-align:middle;margin-right:3px;"></span>无效
  1225. · <b id="s0" style="color:#9ca3af;">0</b></span>
  1226. <span><span class="sw"
  1227. style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#fcd34d;vertical-align:middle;margin-right:3px;"></span>未判
  1228. · <b id="sna" style="color:#b87918;">0</b></span>
  1229. </span>
  1230. </h4>
  1231. <div class="sub" style="font-size:11px;color:#6b7280;margin-bottom:8px;">
  1232. 共 1350 格 · 形式①原词不替换 · ②句子套模板 · ③同义池替换。
  1233. </div>
  1234. <div class="ctl"
  1235. style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:11px;border-top:1px solid #f3f4f6;padding-top:8px;">
  1236. <span class="lab" style="color:#9aa0ad;font-weight:600;">query生成方式</span>
  1237. <span class="g-form">
  1238. <button class="btn on" data-k="form" data-v="A">①原词</button>
  1239. <button class="btn" data-k="form" data-v="B">②句子</button>
  1240. <button class="btn" data-k="form" data-v="C">③同义</button>
  1241. </span>
  1242. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">知识类型</span>
  1243. <span class="g-lens">
  1244. <button class="btn on" data-k="lens" data-v="工序">工序</button>
  1245. <button class="btn" data-k="lens" data-v="工具">工具</button>
  1246. <button class="btn" data-k="lens" data-v="能力">能力</button>
  1247. </span>
  1248. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">约束·工具类型</span>
  1249. <span class="g-tool">
  1250. <button class="btn on" data-k="tool" data-v="">无</button>
  1251. <span id="toolBtns"></span>
  1252. </span>
  1253. <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">搜索优先级</span>
  1254. <span class="g-tier">
  1255. <button class="btn on" data-k="tier" data-v="0">全部</button>
  1256. <button class="btn" data-k="tier" data-v="2">≥中</button>
  1257. <button class="btn" data-k="tier" data-v="3">仅高</button>
  1258. </span>
  1259. </div>
  1260. </div>
  1261. <div class="mxwrap"
  1262. style="max-height:55vh; overflow:auto; border:1px solid var(--line); border-radius: 0 0 8px 8px; margin-bottom:12px; background:#fff; position:relative;">
  1263. <table id="comboMx"></table>
  1264. </div>
  1265. <div class="pop" id="pop"></div>
  1266. <div class="navrow"><span class="navlab">渠道</span>
  1267. <div id="navC" style="display:flex;gap:8px;flex-wrap:wrap"></div>
  1268. </div>
  1269. </div>
  1270. <div class="toolbar">
  1271. <div class="filters"></div>
  1272. <!-- 动态相关性过滤阈值 -->
  1273. <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);">
  1274. <span style="color: var(--muted); font-size: 12px; font-weight: 700;">相关性过滤阈值:</span>
  1275. <input type="number" id="relThreshold" min="0" max="10" step="0.5" value="4.0"
  1276. oninput="renderGrid(); renderHead();"
  1277. 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;"
  1278. onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#d1d5db'">
  1279. </div>
  1280. <button id="reevalBtn" onclick="reevalCurrentQuery()" title="只对当前 query 的所有 form/帖子复评(不重新搜索)">♻️ 重评当前 query</button>
  1281. <button id="editSpecBtn" onclick="openSpecEditor()" title="查看或修改 spec/ 提示词规范文件">📝 编辑 Spec 提示词</button>
  1282. <select id="sort">
  1283. <option value="score">按综合分排序</option>
  1284. <option value="date">按发布时间排序</option>
  1285. <option value="platform">按平台排序</option>
  1286. </select>
  1287. </div>
  1288. <div class="grid" id="grid"></div>
  1289. </div>
  1290. </main>
  1291. <!-- fixed_query_eval:图片放大灯箱(用 dialog 才能叠在详情 modal 之上)-->
  1292. <style>
  1293. #imgLightbox { border:none; background:transparent; padding:0; max-width:100vw; max-height:100vh; overflow:visible; }
  1294. #imgLightbox::backdrop { background: rgba(0,0,0,.85); }
  1295. #imgLightbox img { max-width:92vw; max-height:92vh; object-fit:contain; border-radius:8px; cursor:zoom-out; box-shadow:0 10px 40px rgba(0,0,0,.5); display:block; }
  1296. </style>
  1297. <dialog id="imgLightbox" onclick="this.close()">
  1298. <img id="imgLightboxImg" referrerpolicy="no-referrer" alt="">
  1299. </dialog>
  1300. <!-- fixed_query_eval:批量工具解构 · 选帖弹层 -->
  1301. <dialog id="toolBatchDialog" style="width: min(720px, 92vw); border:1px solid var(--line); border-radius:12px; padding:0;">
  1302. <div style="padding:16px 20px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between; align-items:center;">
  1303. <div>
  1304. <h3 style="margin:0; font-size:17px;">🔧 工具解构 · 选择帖子</h3>
  1305. <div id="toolBatchSub" style="color:var(--muted); font-size:13px; margin-top:2px;"></div>
  1306. </div>
  1307. <button onclick="toolBatchDialog.close()">关闭</button>
  1308. </div>
  1309. <div style="padding:10px 20px; border-bottom:1px solid var(--line); display:flex; gap:14px; align-items:center;">
  1310. <label style="display:flex; gap:6px; align-items:center; cursor:pointer; font-size:13px;">
  1311. <input type="checkbox" id="toolBatchAll" onchange="toolBatchToggleAll(this.checked)"> 全选
  1312. </label>
  1313. <span id="toolBatchCount" style="color:var(--muted); font-size:12px;"></span>
  1314. <span style="flex:1"></span>
  1315. <span style="color:var(--muted); font-size:12px;">模型:gemini-3.1-flash-lite</span>
  1316. </div>
  1317. <div id="toolBatchList" style="max-height:50vh; overflow-y:auto; padding:4px 20px;"></div>
  1318. <div style="padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px;">
  1319. <button onclick="toolBatchDialog.close()">取消</button>
  1320. <button id="toolBatchConfirm" onclick="confirmToolExtract()" style="background:var(--mint); color:#fff; border-color:var(--mint); padding:8px 20px; font-weight:bold; border-radius:8px; cursor:pointer;">确认解构</button>
  1321. </div>
  1322. </dialog>
  1323. <dialog id="detailDialog">
  1324. <div class="modal-head">
  1325. <div>
  1326. <div id="modalMeta" class="meta"></div>
  1327. <h3 id="modalTitle"></h3>
  1328. </div>
  1329. <button onclick="detailDialog.close()">关闭</button>
  1330. </div>
  1331. <div class="modal-tabs" id="modalTabs" style="display: none;">
  1332. <button class="modal-tab active" onclick="switchModalTab('detail')" id="tabDetailBtn">帖子详情</button>
  1333. <button class="modal-tab" onclick="switchModalTab('procedure')" id="tabProcedureBtn">对应工序</button>
  1334. <button class="modal-tab" onclick="switchModalTab('tools')" id="tabToolsBtn">工具解构</button>
  1335. </div>
  1336. <div class="modal-content" id="modalContentDetail">
  1337. <section>
  1338. <div class="rubric-note" id="modalReason"></div>
  1339. <div class="section-title">抓取文本节选</div>
  1340. <div class="raw" id="modalText"></div>
  1341. <div class="section-title">图片预览</div>
  1342. <div class="images" id="modalImages"></div>
  1343. </section>
  1344. <aside>
  1345. <div class="section-title" style="display: flex; justify-content: space-between; align-items: center;">
  1346. <span>评分详情</span>
  1347. <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);">
  1348. 综合评分 <strong style="font-size: 19px; color: #2563eb; font-weight: 900; line-height: 1;" id="modalOverallScoreVal">—</strong>
  1349. </span>
  1350. </div>
  1351. <div class="scores" id="modalScores"></div>
  1352. <div class="section-title">类型 / 命中 query</div>
  1353. <div class="tags" id="modalTags"></div>
  1354. </aside>
  1355. </div>
  1356. <!-- fixed_query_eval:工具解构 tab 内容(JS 填充)-->
  1357. <div id="modalContentTools" style="display: none; min-height: 400px; max-height: calc(100vh - 180px); overflow-y: auto; padding: 16px; box-sizing: border-box;"></div>
  1358. <div id="modalContentProcedure" style="display: none; min-height: 500px; height: calc(100vh - 180px); max-height: 700px; padding: 16px; box-sizing: border-box; flex-direction: column;">
  1359. <!-- Top Action Bar (inside tab) -->
  1360. <div id="procActionBar" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); flex-shrink: 0;">
  1361. <div style="font-size: 14px; font-weight: bold; color: var(--ink);" id="procStatusText">工序状态: 检测中...</div>
  1362. <div style="display: flex; gap: 8px;" id="procActionBtns">
  1363. <!-- Dynamically populated buttons (Regenerate, View Logs, etc) -->
  1364. </div>
  1365. </div>
  1366. <!-- Main view container -->
  1367. <div style="flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column;">
  1368. <!-- Configuration / Setup panel (when not generated) -->
  1369. <div id="procSetupPanel" style="display: none; flex-direction: column; align-items: center; justify-content: center; text-align: center; height: 100%; padding: 40px 20px;">
  1370. <div style="font-size: 36px; margin-bottom: 16px;">✨</div>
  1371. <h4 style="margin: 0 0 10px; font-size: 18px;">提取本帖工序</h4>
  1372. <p style="color: var(--muted); font-size: 14px; max-width: 500px; margin: 0 0 24px;">该帖子目前尚未生成对应的结构化工序。请在下方选择提取引擎和模型,点击开始提取。</p>
  1373. <div style="display: flex; gap: 16px; margin-bottom: 24px; text-align: left; background: #fff; border: 1px solid var(--line); padding: 16px; border-radius: 8px; box-shadow: var(--shadow);">
  1374. <div>
  1375. <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">提取引擎 (Engine)</label>
  1376. <select id="procEngineSelect" style="min-width: 180px; padding: 6px 10px;" onchange="onProcEngineChange()">
  1377. <option value="cyber_runner">Cyber Runner (自研/OpenRouter)</option>
  1378. <option value="claude_sdk">Claude SDK (OAuth)</option>
  1379. </select>
  1380. </div>
  1381. <div>
  1382. <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">AI 模型 (Model)</label>
  1383. <select id="procModelSelect" style="min-width: 240px; padding: 6px 10px;">
  1384. <!-- Dynamically populated options -->
  1385. </select>
  1386. </div>
  1387. </div>
  1388. <button id="startProcBtn" onclick="startProcedureExtraction()" style="background: var(--mint); color: #fff; border-color: var(--mint); padding: 10px 24px; font-size: 15px; font-weight: bold; border-radius: 8px; cursor: pointer; transition: all 0.2s;">开始提取工序</button>
  1389. </div>
  1390. <!-- Live Log Terminal View -->
  1391. <div id="procConsolePanel" style="display: none; flex-direction: column; height: 100%; min-height: 0; background: #1e1b18; border: 1px solid #3d352e; border-radius: 8px; overflow: hidden; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);">
  1392. <!-- Console header -->
  1393. <div style="background: #2b2520; padding: 6px 12px; border-bottom: 1px solid #3d352e; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; font-family: monospace; font-size: 12px; color: #a3968d;">
  1394. <span>TERMINAL CONSOLE</span>
  1395. <span id="procConsoleStatus">idle</span>
  1396. </div>
  1397. <!-- Console log output -->
  1398. <pre id="procConsoleOutput" style="flex: 1; margin: 0; padding: 12px; overflow: auto; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; font-size: 13px; line-height: 1.45; color: #cbbba9; background: #1e1b18; white-space: pre-wrap; word-break: break-all;"></pre>
  1399. </div>
  1400. <!-- Iframe Panel for completed HTML -->
  1401. <iframe id="procedureIframe" style="display: none; width: 100%; height: 100%; border: 1px solid var(--line); border-radius: 8px; background: #fff;" referrerpolicy="no-referrer"></iframe>
  1402. </div>
  1403. </div>
  1404. </dialog>
  1405. <!-- Spec Editor Dialog -->
  1406. <dialog id="specEditorDialog" style="width: 850px; max-width: 95%; border: 1px solid var(--line); border-radius: 12px; padding: 0; box-shadow: var(--shadow); background: var(--panel);">
  1407. <div style="display: flex; justify-content: space-between; align-items: center; background: #faf7f1; border-bottom: 1px solid var(--line); padding: 16px 20px;">
  1408. <h3 style="margin: 0; font-size: 18px; color: var(--ink); font-weight: 800;">📝 编辑 Spec 提示词规范</h3>
  1409. <button onclick="document.getElementById('specEditorDialog').close()" style="background: none; border: 1px solid var(--line); padding: 5px 14px; border-radius: 8px; cursor: pointer; color: var(--muted); font-weight: 600; font-size: 12px;">关闭</button>
  1410. </div>
  1411. <div style="padding: 20px;">
  1412. <div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
  1413. <span style="font-size: 13.5px; font-weight: 700; color: var(--ink);">选择提示词文件:</span>
  1414. <select id="specFileSelect" onchange="loadSpecFileContent()" style="padding: 6px 12px; border-radius: 8px; border: 1px solid #d1d5db; outline: none; min-width: 320px; font-family: monospace; font-size: 13px; font-weight: 600;">
  1415. <!-- Spec files options will be populated -->
  1416. </select>
  1417. <span id="specLoadStatus" style="font-size: 13px; font-weight: 600;"></span>
  1418. </div>
  1419. <div style="position: relative; margin-bottom: 20px;">
  1420. <textarea id="specContentTextarea" style="width: 100%; height: 500px; font-family: ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.55; padding: 14px; border: 1px solid #d1d5db; border-radius: 8px; box-sizing: border-box; outline: none; background: #fafaf9; color: #1f2937; resize: vertical; border-left: 4px solid var(--cyan); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);"></textarea>
  1421. </div>
  1422. <div style="display: flex; justify-content: flex-end; gap: 12px; align-items: center; border-top: 1px solid var(--line); padding-top: 16px;">
  1423. <span id="specSaveStatus" style="font-size: 13.5px; font-weight: 700; margin-right: auto;"></span>
  1424. <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="background: #f3f4f6; height: 38px; padding: 0 18px; border-radius: 8px; font-weight: 600;">取消</button>
  1425. <button onclick="saveSpecFileContent()" class="btn" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: bold; height: 38px; padding: 0 20px; border-radius: 8px; box-shadow: var(--shadow);">💾 保存修改</button>
  1426. </div>
  1427. </div>
  1428. </dialog>
  1429. <script>
  1430. let DATA = { queries: [], actions: [], types: [], matrix: [] }, st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full' }, VIEW = [];
  1431. let currentProcTask = null;
  1432. let procPollInterval = null;
  1433. let isLogViewActive = false;
  1434. let reevalPollIntervals = {};
  1435. function startReevalPolling(q) {
  1436. if (reevalPollIntervals[q]) return;
  1437. const btn = document.getElementById('reevalBtn');
  1438. const poll = () => {
  1439. fetch(`/api/reeval_status?q=${q}`)
  1440. .then(r => r.json())
  1441. .then(d => {
  1442. const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
  1443. if (d.status === "success") {
  1444. clearInterval(reevalPollIntervals[q]);
  1445. delete reevalPollIntervals[q];
  1446. if (isCurrent) {
  1447. btn.disabled = false;
  1448. btn.textContent = '♻️ 重评当前 query';
  1449. }
  1450. // Mark local status as finished
  1451. if (DATA.active_reevals) {
  1452. delete DATA.active_reevals[q];
  1453. }
  1454. loadData(true);
  1455. alert(`Query ${q} 重评完成!已自动重新扫描并更新数据。`);
  1456. } else if (d.status === "failed") {
  1457. clearInterval(reevalPollIntervals[q]);
  1458. delete reevalPollIntervals[q];
  1459. if (isCurrent) {
  1460. btn.disabled = false;
  1461. btn.textContent = '♻️ 重评当前 query';
  1462. }
  1463. if (DATA.active_reevals) {
  1464. delete DATA.active_reevals[q];
  1465. }
  1466. alert(`Query ${q} 重评失败:${d.error}`);
  1467. } else if (d.status === "running") {
  1468. if (isCurrent) {
  1469. btn.disabled = true;
  1470. btn.textContent = `♻️ 重评中 ${q}...`;
  1471. }
  1472. }
  1473. })
  1474. .catch(err => console.error("Poll error:", err));
  1475. };
  1476. poll();
  1477. reevalPollIntervals[q] = setInterval(poll, 3000);
  1478. }
  1479. // Spec Prompt Editor Functions
  1480. const ALLOWED_SPEC_FILES = [
  1481. "README.md",
  1482. "tools.md",
  1483. "extraction/phase1-skeleton.md",
  1484. "extraction/phase2-normalize.md",
  1485. "extraction/phase3-finalize.md",
  1486. "taxonomy/type_suggestions.md"
  1487. ];
  1488. function openSpecEditor() {
  1489. const select = document.getElementById("specFileSelect");
  1490. select.innerHTML = ALLOWED_SPEC_FILES.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
  1491. document.getElementById("specLoadStatus").textContent = "";
  1492. document.getElementById("specSaveStatus").textContent = "";
  1493. document.getElementById("specContentTextarea").value = "";
  1494. const dialog = document.getElementById("specEditorDialog");
  1495. dialog.showModal();
  1496. loadSpecFileContent();
  1497. }
  1498. function loadSpecFileContent() {
  1499. const file = document.getElementById("specFileSelect").value;
  1500. const status = document.getElementById("specLoadStatus");
  1501. const textarea = document.getElementById("specContentTextarea");
  1502. status.textContent = "⏳ 正在读取...";
  1503. status.style.color = "var(--amber)";
  1504. textarea.disabled = true;
  1505. fetch(`/api/spec_content?file=${encodeURIComponent(file)}`)
  1506. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  1507. .then(({ ok, d }) => {
  1508. textarea.disabled = false;
  1509. if (ok) {
  1510. textarea.value = d.content || "";
  1511. status.textContent = "✓ 读取成功";
  1512. status.style.color = "var(--mint)";
  1513. } else {
  1514. status.textContent = "❌ 读取失败: " + (d.error || "未知错误");
  1515. status.style.color = "var(--rose)";
  1516. }
  1517. })
  1518. .catch(err => {
  1519. textarea.disabled = false;
  1520. status.textContent = "❌ 读取失败: " + err;
  1521. status.style.color = "var(--rose)";
  1522. });
  1523. }
  1524. function saveSpecFileContent() {
  1525. const file = document.getElementById("specFileSelect").value;
  1526. const content = document.getElementById("specContentTextarea").value;
  1527. const status = document.getElementById("specSaveStatus");
  1528. status.textContent = "⏳ 正在保存到磁盘...";
  1529. status.style.color = "var(--amber)";
  1530. fetch("/api/save_spec", {
  1531. method: "POST",
  1532. headers: { "Content-Type": "application/json" },
  1533. body: JSON.stringify({ file, content })
  1534. })
  1535. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  1536. .then(({ ok, d }) => {
  1537. if (ok) {
  1538. status.textContent = "✅ 保存成功!新的 Prompt 规则已在本地生效。";
  1539. status.style.color = "var(--mint)";
  1540. setTimeout(() => {
  1541. if (status.textContent.includes("保存成功")) {
  1542. status.textContent = "";
  1543. }
  1544. }, 4000);
  1545. } else {
  1546. status.textContent = "❌ 保存失败: " + (d.error || "未知错误");
  1547. status.style.color = "var(--rose)";
  1548. }
  1549. })
  1550. .catch(err => {
  1551. status.textContent = "❌ 网络错误: " + err;
  1552. status.style.color = "var(--rose)";
  1553. });
  1554. }
  1555. 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;"); }
  1556. 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": "变" }];
  1557. 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": "知识库" }];
  1558. 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": ["代码", "命令行"], "插件扩展": ["插件"] } };
  1559. const TOOL_TYPES = ["AI 模型", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"];
  1560. const GMODEL = "gemini-3.1-flash-lite";
  1561. const AL = POOLS.action_leaves, TP = POOLS.types, KN = POOLS.knowledge, TQ = POOLS.tool_type;
  1562. function aPool(a) { return AL[a] || [a]; }
  1563. function tPool(t) { return TP[t] || [t]; }
  1564. function pick(arr, i) { return arr[Math.min(i, arr.length - 1)]; }
  1565. function toolPrefix(lens) { return (st.tools.length && lens !== '工具') ? st.tools.map(t => TQ[t][0]).join('/') + ' ' : ''; }
  1566. function genForms(leaf, ty, lens, tq) {
  1567. tq = tq || ''; const aP = aPool(leaf), tP = tPool(ty);
  1568. const aNat = aP[0], tNat = tP[0], aSyn = pick(aP, 1), tSyn = pick(tP, 1), K = KN[lens];
  1569. if (lens === '工序') {
  1570. const s = K['单步'];
  1571. return [['①原词', `${tq}${leaf} ${ty} 流程`], ['②句子', `${tq}怎么${aNat}${tNat}`], ['③同义', `${tq}${aSyn} ${tSyn} ${pick(s, 2)}`]];
  1572. }
  1573. if (lens === '能力') {
  1574. const mk = K['标记'];
  1575. return [['①原词', `${tq}${leaf} ${ty} 技巧`], ['②句子', `${tq}有没有能${mk[0]}${aNat}${tNat}的功能`], ['③同义', `${tq}${pick(mk, 1)} ${aSyn}${tSyn}`]];
  1576. }
  1577. const fd = K['发现'];
  1578. return [['①原词', `${leaf} ${ty} 工具`], ['②句子', `${aNat}${tNat}用什么软件好`], ['③同义', `${aSyn} ${tSyn} ${pick(fd, 2)}`]];
  1579. }
  1580. const FIDX = { A: 0, B: 1, C: 2 };
  1581. function genQ(leaf, ty) { return genForms(leaf, ty, st.lens, toolPrefix(st.lens))[FIDX[st.form]][1]; }
  1582. // 兼容老版 1-5 维度的英文和中文对应标签
  1583. 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: "限制说明" };
  1584. const filterLabels = { production_relevance: "制作相关性", recency_hard: "发布时效", overall: "综合均分" };
  1585. const filterMax = { production_relevance: 3, recency_hard: 3, overall: 5 };
  1586. // 新版 0-10 分数维度的中文标签
  1587. const newLabels = {
  1588. relevance_production: "和内容制作知识相关",
  1589. relevance_query: "和 query 相关",
  1590. recency: "时效性",
  1591. popularity: "热度性",
  1592. feedback: "评论反馈",
  1593. realism: "真实感 (非AI)",
  1594. expressiveness: "表现力",
  1595. procedure_completeness: "流程完整性",
  1596. procedure_input: "输入完整性",
  1597. procedure_implementation: "实现完整性",
  1598. procedure_output: "输出完整性",
  1599. procedure_generality: "泛化性",
  1600. step_input: "输入完整性",
  1601. step_implementation: "实现完整性",
  1602. step_output: "输出完整性",
  1603. step_generality: "泛化性",
  1604. tool_boundary: "能力边界覆盖",
  1605. tool_comparison: "有效比较",
  1606. tool_specificity: "参数/接口具体性",
  1607. tool_example: "实操示例",
  1608. tool_limits: "版本&限制"
  1609. };
  1610. const scoreGroupsOld = [
  1611. { id: "filter", title: "过滤指标", short: "过滤", hint: "独立于通用维度·过滤逻辑待定", topLevelKeys: ["production_relevance", "recency_hard", "overall"] },
  1612. { id: "common", title: "通用维度", short: "通用", keys: ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"] },
  1613. { id: "procedure", title: "工序维度", short: "工序", keys: ["completeness", "step_structure", "step_reproducibility"] },
  1614. { id: "step", title: "步骤维度", short: "步骤", keys: ["capability_definition", "implementation_depth", "boundary_failure_eval", "generality"] },
  1615. { id: "tool", title: "工具维度", short: "工具", keys: ["capability_coverage", "effective_comparison", "param_specificity", "worked_example", "version_limits"] }
  1616. ];
  1617. const scoreGroupsNew = [
  1618. { id: "relevance", title: "相关性", short: "相关", keys: ["relevance_production", "relevance_query"] },
  1619. { id: "fixed", title: "固定维度", short: "固定", keys: ["recency", "popularity", "feedback"] },
  1620. { id: "usecase", title: "用例维度", short: "用例", keys: ["realism", "expressiveness"] },
  1621. { id: "dynamic", title: "动态维度", short: "动态", keys: [] }
  1622. ];
  1623. function makeRow(label, scoreKey, it) {
  1624. const rawV = it.scores[scoreKey];
  1625. const v = (rawV !== undefined && rawV !== null) ? parseFloat(rawV) : NaN;
  1626. const hasScore = !isNaN(v);
  1627. const valStr = hasScore ? (Number.isInteger(v) ? v : v.toFixed(1)) : '-';
  1628. const barV = hasScore ? v : 0;
  1629. const reason = it.score_reasons ? it.score_reasons[scoreKey] : '';
  1630. 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>` : '';
  1631. return `
  1632. <div class="sc-row ${!hasScore ? 'missing' : ''}">
  1633. <span class="label">${esc(label)}</span>
  1634. <div class="bar-wrap">
  1635. <div class="bar">
  1636. <div class="bar-fill" style="--v: ${barV}"></div>
  1637. </div>
  1638. <span class="value">${valStr}</span>
  1639. ${infoIcon}
  1640. </div>
  1641. </div>
  1642. `;
  1643. }
  1644. function getQualityAverage(it) {
  1645. if (!it.scores) return null;
  1646. const keys = ["recency", "popularity", "feedback", "realism", "expressiveness"];
  1647. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1648. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1649. }
  1650. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1651. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1652. }
  1653. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1654. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1655. }
  1656. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1657. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1658. }
  1659. function renderNewScores(it) {
  1660. // 1. Relevance Card
  1661. const relAvg = groupAverage(it, scoreGroupsNew[0], true);
  1662. const relAvgStr = relAvg !== null ? relAvg.toFixed(1) : 'N/A';
  1663. let relevanceHtml = `
  1664. <div class="sc-card">
  1665. <div class="sc-card-head">
  1666. <div class="title"><span class="badge">01</span>相关性</div>
  1667. <div class="avg-score">avg <strong>${relAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
  1668. </div>
  1669. <div class="sc-card-body">
  1670. ${makeRow("和内容制作知识相关", "relevance_production", it)}
  1671. ${makeRow("和 query 相关", "relevance_query", it)}
  1672. </div>
  1673. </div>
  1674. `;
  1675. // 2. Quality Card
  1676. const qualAvg = getQualityAverage(it);
  1677. const qualAvgStr = qualAvg !== null ? qualAvg.toFixed(1) : 'N/A';
  1678. let qualityHtml = `
  1679. <div class="sc-card">
  1680. <div class="sc-card-head">
  1681. <div class="title"><span class="badge">02</span>质量</div>
  1682. <div class="avg-score">avg <strong>${qualAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
  1683. </div>
  1684. <div class="sc-card-body">
  1685. <div class="sc-sub-header">固定维度</div>
  1686. ${makeRow("时效性", "recency", it)}
  1687. ${makeRow("热度性", "popularity", it)}
  1688. ${makeRow("评论反馈", "feedback", it)}
  1689. <div class="sc-sub-header">用例</div>
  1690. ${makeRow("真实感 (非AI)", "realism", it)}
  1691. ${makeRow("表现力", "expressiveness", it)}
  1692. `;
  1693. // Dynamic Dimensions
  1694. let dynamicHtml = '';
  1695. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1696. dynamicHtml += `
  1697. <div class="sc-sub-header">工序</div>
  1698. ${makeRow("流程完整性", "procedure_completeness", it)}
  1699. ${makeRow("输入完整性", "procedure_input", it)}
  1700. ${makeRow("实现完整性", "procedure_implementation", it)}
  1701. ${makeRow("输出完整性", "procedure_output", it)}
  1702. ${makeRow("泛化性", "procedure_generality", it)}
  1703. `;
  1704. }
  1705. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1706. dynamicHtml += `
  1707. <div class="sc-sub-header">能力</div>
  1708. ${makeRow("输入完整性", "step_input", it)}
  1709. ${makeRow("实现完整性", "step_implementation", it)}
  1710. ${makeRow("输出完整性", "step_output", it)}
  1711. ${makeRow("泛化性", "step_generality", it)}
  1712. `;
  1713. }
  1714. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1715. dynamicHtml += `
  1716. <div class="sc-sub-header">工具</div>
  1717. ${makeRow("能力边界覆盖", "tool_boundary", it)}
  1718. ${makeRow("有效比较", "tool_comparison", it)}
  1719. ${makeRow("参数/接口具体性", "tool_specificity", it)}
  1720. ${makeRow("实操示例", "tool_example", it)}
  1721. ${makeRow("版本&限制", "tool_limits", it)}
  1722. `;
  1723. }
  1724. qualityHtml += dynamicHtml + `
  1725. </div>
  1726. </div>
  1727. `;
  1728. return relevanceHtml + qualityHtml;
  1729. }
  1730. function isItemDiscarded(it) {
  1731. if (it.anomaly) return false;
  1732. const input = document.getElementById("relThreshold");
  1733. const userThreshold = input ? parseFloat(input.value) : NaN;
  1734. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  1735. let isDiscard = false;
  1736. // 1. Relevance check
  1737. const relVal = it.production_relevance !== null && it.production_relevance !== undefined ? parseFloat(it.production_relevance) : null;
  1738. if (relVal !== null && !isNaN(relVal)) {
  1739. const activeThreshold = !isNaN(userThreshold) ? userThreshold : (isNewSchema ? 4.0 : 2.0);
  1740. if (relVal < activeThreshold) {
  1741. isDiscard = true;
  1742. }
  1743. }
  1744. // 2. Recency check
  1745. if (it.recency_hard !== null && it.recency_hard !== undefined && it.recency_hard < 2) {
  1746. isDiscard = true;
  1747. }
  1748. // 3. Overall average check
  1749. if (it.overall !== null && it.overall !== undefined) {
  1750. const threshold_ov = isNewSchema ? 6.0 : 3.0;
  1751. if (it.overall < threshold_ov) {
  1752. isDiscard = true;
  1753. }
  1754. }
  1755. return isDiscard;
  1756. }
  1757. function updateThresholdLimits() {
  1758. const f = curForm();
  1759. const input = document.getElementById("relThreshold");
  1760. if (!f || !f.results || f.results.length === 0 || !input) return;
  1761. const isNew = f.results.some(r => r.scores && (r.scores.relevance_production !== undefined || r.scores.relevance_query !== undefined));
  1762. const schemaKey = isNew ? "new" : "old";
  1763. if (input.dataset.schema !== schemaKey) {
  1764. input.dataset.schema = schemaKey;
  1765. input.max = isNew ? "10" : "5";
  1766. input.value = isNew ? "4.0" : "2.0";
  1767. input.step = isNew ? "0.5" : "1";
  1768. }
  1769. }
  1770. const PLATC = { xhs: "小红书", gzh: "公众号", zhihu: "知乎", x: "X", bili: "B站", douyin: "抖音", sph: "视频号", youtube: "YouTube", github: "GitHub", toutiao: "头条", weibo: "微博" };
  1771. const FN = { A: "原词", B: "句子", C: "同义" };
  1772. 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";
  1773. function curForm() { return st.qi === -1 ? null : (DATA.queries[st.qi] ? DATA.queries[st.qi].forms[st.fi] : null); }
  1774. function groupAverage(it, g, isNewSchema) {
  1775. if (!it.scores) return null;
  1776. let keys = g.keys;
  1777. if (isNewSchema && g.id === "dynamic") {
  1778. keys = [];
  1779. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1780. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1781. }
  1782. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1783. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1784. }
  1785. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1786. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1787. }
  1788. }
  1789. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1790. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1791. }
  1792. function fmt(v) { return v === null ? "N/A" : v.toFixed(1); }
  1793. function groupSnapshot(it) {
  1794. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  1795. if (isNewSchema) {
  1796. const relAvg = groupAverage(it, scoreGroupsNew[0], true);
  1797. const qualAvg = getQualityAverage(it);
  1798. return `
  1799. <div class="group-pill"><span>相关</span><strong>${fmt(relAvg)}</strong></div>
  1800. <div class="group-pill"><span>质量</span><strong>${fmt(qualAvg)}</strong></div>
  1801. `;
  1802. }
  1803. const groups = scoreGroupsOld;
  1804. return groups.map(g => {
  1805. if (!isNewSchema && g.id === 'filter') {
  1806. const pr = it.production_relevance, rh = it.recency_hard, ov = it.overall;
  1807. const f = (v, max) => (v == null || !Number.isFinite(v)) ? '-' : (max === 5 ? (typeof v === 'number' ? v.toFixed(1) : v) : v);
  1808. 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>`;
  1809. }
  1810. const a = groupAverage(it, g, isNewSchema);
  1811. return `<div class="group-pill"><span>${g.short}</span><strong>${fmt(a)}</strong></div>`;
  1812. }).join("");
  1813. }
  1814. // filter 组:数据从 it 顶层、量程按 filterMax 归一化到 5 量程(meter 的 --v*20% 公式不动)
  1815. function renderScoreGroup(it, g) {
  1816. const isFilter = g.id === 'filter';
  1817. const keys = g.topLevelKeys || g.keys;
  1818. const src = isFilter ? it : it.scores;
  1819. if (!src) return '';
  1820. const labels = isFilter ? filterLabels : commonLabels;
  1821. const rows = keys.map(k => {
  1822. const v = src[k];
  1823. const m = !Number.isFinite(v);
  1824. const max = isFilter ? (filterMax[k] || 5) : 5;
  1825. const meterV = m ? 0 : (v * 5 / max);
  1826. const suffix = isFilter && max !== 5 ? `/${max}` : '';
  1827. const valStr = m ? '-' : (typeof v === 'number' ? (Number.isInteger(v) ? v : v.toFixed(1)) : v);
  1828. const reason = (it.score_reasons) ? it.score_reasons[k] : '';
  1829. 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>` : '';
  1830. 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>`;
  1831. }).join("");
  1832. const headRight = isFilter ? (g.hint ? `<small>${g.hint}</small>` : '') : `<small>均分 ${fmt(groupAverage(it, g))}</small>`;
  1833. return `<section class="score-group"><div class="score-group-head"><span>${g.title}</span>${headRight}</div><div class="score-group-body">${rows}</div></section>`;
  1834. }
  1835. function curDims() {
  1836. 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 {}; }
  1837. const q = DATA.queries[st.qi]; return (q && q.dims) ? q.dims : {};
  1838. }
  1839. 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; }
  1840. const l1sp = spans(ACTIONS, 'l1'), l2sp = spans(ACTIONS, 'l2');
  1841. const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
  1842. function detectLens(q) {
  1843. const text = (q.original_q || (q.forms && q.forms[0] && q.forms[0].query) || "").toLowerCase();
  1844. if (/流程|步骤|教程|方法|教学|SOP|pipeline|工序/.test(text)) return '工序';
  1845. if (/一键|自动|直出|秒出|同款|复刻|效果|技巧|能力/.test(text)) return '能力';
  1846. if (/用什么|软件|工具|推荐|哪个好用|有哪些/.test(text)) return '工具';
  1847. return '工序';
  1848. }
  1849. function findQuery(aName, tName, lens, tool) {
  1850. const matches = DATA.queries.filter(q => {
  1851. if (!q.dims || q.dims.action !== aName || q.dims.type !== tName) return false;
  1852. const qLens = detectLens(q);
  1853. if (qLens !== lens) return false;
  1854. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  1855. if (tool) {
  1856. if (!hasToolConstraint || q.dims.constraint.value !== tool) return false;
  1857. }
  1858. return true;
  1859. });
  1860. if (matches.length > 0) {
  1861. matches.sort((x, y) => {
  1862. if (!tool) {
  1863. const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
  1864. const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
  1865. if (hasX !== hasY) return hasX ? 1 : -1;
  1866. }
  1867. return (y.hits || 0) - (x.hits || 0);
  1868. });
  1869. return matches[0];
  1870. }
  1871. return null;
  1872. }
  1873. function selectQueryByActiveCellAndControls(aName, tName) {
  1874. const activeTool = st.tools[0] || null;
  1875. let match = findQuery(aName, tName, st.lens, activeTool);
  1876. const isProcedureView = st.matrixView === 'procedures';
  1877. const isHitsView = st.matrixView === 'hits';
  1878. const hasContent = q => {
  1879. if (isProcedureView) return getFormProcedureCount(q) > 0;
  1880. if (isHitsView) return getFormReportCount(q) > 0;
  1881. return q.hits > 0;
  1882. };
  1883. if (match) {
  1884. st.qi = DATA.queries.indexOf(match);
  1885. const fi = match.forms.findIndex(f => f.form === st.form);
  1886. st.fi = fi >= 0 ? fi : 0;
  1887. st.selectedAction = aName;
  1888. st.selectedType = tName;
  1889. } else {
  1890. // Fallback: search for any query matching active lens & tool constraint with hits
  1891. const matches = DATA.queries.filter(q => {
  1892. if (!q.dims || !q.dims.action || !q.dims.type) return false;
  1893. const qLens = detectLens(q);
  1894. if (qLens !== st.lens) return false;
  1895. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  1896. if (activeTool) {
  1897. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  1898. } else {
  1899. if (hasToolConstraint) return false;
  1900. }
  1901. return hasContent(q);
  1902. });
  1903. if (matches.length > 0) {
  1904. matches.sort((x, y) => {
  1905. if (!activeTool) {
  1906. const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
  1907. const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
  1908. if (hasX !== hasY) return hasX ? 1 : -1;
  1909. }
  1910. const countX = isProcedureView ? getFormProcedureCount(x) : (isHitsView ? getFormReportCount(x) : x.hits);
  1911. const countY = isProcedureView ? getFormProcedureCount(y) : (isHitsView ? getFormReportCount(y) : y.hits);
  1912. return countY - countX;
  1913. });
  1914. const anyMatch = matches[0];
  1915. st.qi = DATA.queries.indexOf(anyMatch);
  1916. const fi = anyMatch.forms.findIndex(f => f.form === st.form);
  1917. st.fi = fi >= 0 ? fi : 0;
  1918. st.selectedAction = anyMatch.dims.action;
  1919. st.selectedType = anyMatch.dims.type;
  1920. } else {
  1921. st.qi = -1;
  1922. st.fi = 0;
  1923. }
  1924. }
  1925. }
  1926. function getFormReportCount(q) {
  1927. const f = q.forms && q.forms.find(x => x.form === st.form);
  1928. if (!f || !f.results) return 0;
  1929. return f.results.filter(r => !r.anomaly && !isItemDiscarded(r)).length;
  1930. }
  1931. function getFormProcedureCount(q) {
  1932. const f = q.forms && q.forms.find(x => x.form === st.form);
  1933. if (!f || !f.results) return 0;
  1934. return f.results.filter(r => !r.anomaly && !isItemDiscarded(r) && r.procedure_html).length;
  1935. }
  1936. function getFilteredHitsMap() {
  1937. const cm = {};
  1938. const activeTool = st.tools[0] || null;
  1939. const isProcedureView = st.matrixView === 'procedures';
  1940. DATA.queries.forEach((q, i) => {
  1941. const d = q.dims;
  1942. if (!d || !d.type || !d.action) return;
  1943. // Filter by lens
  1944. if (detectLens(q) !== st.lens) return;
  1945. // Filter by active tool constraint
  1946. const hasToolConstraint = d.constraint && d.constraint.kind === "工具类型";
  1947. if (activeTool) {
  1948. if (!hasToolConstraint || d.constraint.value !== activeTool) return;
  1949. } else {
  1950. if (hasToolConstraint) return;
  1951. }
  1952. const k = d.type + '|' + d.action;
  1953. const h = isProcedureView ? getFormProcedureCount(q) : getFormReportCount(q);
  1954. if (!cm[k] || h > cm[k].hits) {
  1955. cm[k] = { i, hits: h };
  1956. }
  1957. });
  1958. return cm;
  1959. }
  1960. function renderMatrix() {
  1961. const viewMode = st.matrixView || 'full';
  1962. const showFull = viewMode === 'full';
  1963. const cm = getFilteredHitsMap();
  1964. const activeActions = showFull ? ACTIONS : ACTIONS.filter(a => TYPES.some(t => {
  1965. const c = cm[t.name + '|' + a.name];
  1966. return c && c.hits > 0;
  1967. }));
  1968. const activeTypes = showFull ? TYPES : TYPES.filter(t => ACTIONS.some(a => {
  1969. const c = cm[t.name + '|' + a.name];
  1970. return c && c.hits > 0;
  1971. }));
  1972. const displayActions = activeActions.length ? activeActions : ACTIONS;
  1973. const displayTypes = activeTypes.length ? activeTypes : TYPES;
  1974. const l1sp = spans(displayActions, 'l1'), l2sp = spans(displayActions, 'l2');
  1975. const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
  1976. 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>';
  1977. h += '<tr class="l2">' + l2sp.map(([v, s, c]) => `<th colspan="${c}" class="${l1Start.has(s) ? 'l1div' : 'l2div'}">${v}</th>`).join('') + '</tr>';
  1978. 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>';
  1979. const typeCategories = ['程序控制类型', '数据复用类型', '内容类型', '知识类型'];
  1980. typeCategories.forEach(l1 => {
  1981. const catTypes = displayTypes.filter(t => t.l1 === l1);
  1982. if (catTypes.length === 0) return;
  1983. h += `<tr class="l1row"><td colspan="${displayActions.length + 1}">${l1}</td></tr>`;
  1984. catTypes.forEach((t) => {
  1985. h += `<tr data-type="${t.name}"><th class="rh" data-ti="${TYPES.indexOf(t)}">${t.name}</th>` + displayActions.map((a) => {
  1986. const ai = ACTIONS.indexOf(a);
  1987. const ti = TYPES.indexOf(t);
  1988. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  1989. const s = cell.tier !== undefined ? cell.tier : cell.s;
  1990. const cls = (s === null || s === undefined) ? 'tNA' : ('t' + s);
  1991. const isSel = (st.selectedAction === a.name && st.selectedType === t.name) ? ' sel' : '';
  1992. return `<td class="cell ${cls}${isSel}" data-ai="${ai}" data-ti="${ti}"></td>`;
  1993. }).join('') + '</tr>';
  1994. });
  1995. });
  1996. document.getElementById('comboMx').innerHTML = h + '</tbody>';
  1997. refresh();
  1998. applyCrosshair();
  1999. }
  2000. // 点中 cell 时浅高亮整行整列(含 row/col 表头),方便定位
  2001. function applyCrosshair() {
  2002. document.querySelectorAll('.rowsel,.colsel').forEach(el => el.classList.remove('rowsel', 'colsel'));
  2003. const ai = st.selectedAction ? ACTIONS.findIndex(a => a.name === st.selectedAction) : -1;
  2004. const ti = st.selectedType ? TYPES.findIndex(t => t.name === st.selectedType) : -1;
  2005. if (ai < 0 && ti < 0) return;
  2006. document.querySelectorAll(`#comboMx [data-ai="${ai}"]`).forEach(el => el.classList.add('colsel'));
  2007. document.querySelectorAll(`#comboMx [data-ti="${ti}"]`).forEach(el => el.classList.add('rowsel'));
  2008. }
  2009. function refresh() {
  2010. const cm = getFilteredHitsMap();
  2011. document.querySelectorAll('tr[data-type]').forEach(tr => {
  2012. const ty = tr.dataset.type;
  2013. tr.querySelectorAll('td.cell').forEach(td => {
  2014. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai];
  2015. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  2016. const s = cell.tier !== undefined ? cell.tier : cell.s;
  2017. const c = cm[ty + '|' + a.name];
  2018. const hits = c ? c.hits : 0;
  2019. td.textContent = genQ(a.name, ty);
  2020. if (hits > 0) {
  2021. const badge = document.createElement('span');
  2022. badge.className = 'hit-badge';
  2023. badge.textContent = hits;
  2024. td.appendChild(badge);
  2025. }
  2026. td.classList.toggle('hide', (s == null ? 0 : s) < (+st.tier));
  2027. const hitsLabel = st.matrixView === 'procedures' ? '有工序' : '命中';
  2028. td.title = `${ty} × ${a.name}\nGemini评分: ${(s === null || s === undefined) ? '未判' : ['无效', '低', '中', '高'][s]}\n当前 form(${st.form}) ${hitsLabel}: ${hits} 篇`;
  2029. });
  2030. });
  2031. }
  2032. const pop = document.getElementById('pop'); let openCell = null;
  2033. function showPop(td, x, y) {
  2034. openCell = td;
  2035. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
  2036. const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
  2037. const s = cell.tier !== undefined ? cell.tier : cell.s;
  2038. const tn = (s == null) ? '未判' : ['无效', '低', '中', '高'][s], tb = (s == null) ? 'NA' : s;
  2039. const gen = genForms(a.name, t.name, st.lens, toolPrefix(st.lens));
  2040. // Find matching query in database for this cell
  2041. const activeTool = st.tools[0] || null;
  2042. const matches = DATA.queries.filter(q => {
  2043. if (!q.dims || q.dims.action !== a.name || q.dims.type !== t.name) return false;
  2044. const qLens = detectLens(q);
  2045. if (qLens !== st.lens) return false;
  2046. const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
  2047. if (activeTool) {
  2048. if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
  2049. } else {
  2050. if (hasToolConstraint) return false;
  2051. }
  2052. return true;
  2053. });
  2054. let dbQuery = null;
  2055. if (matches.length > 0) {
  2056. matches.sort((x, y) => (y.hits || 0) - (x.hits || 0));
  2057. dbQuery = matches[0];
  2058. }
  2059. const dbFormMap = {};
  2060. if (dbQuery) {
  2061. dbQuery.forms.forEach(f => {
  2062. dbFormMap[f.form] = f.query;
  2063. });
  2064. }
  2065. const formKeys = ['A', 'B', 'C'];
  2066. let html = `<span class="close">×</span>
  2067. <div class="pt">
  2068. <span class="path a">${a.l1}›${a.l2}›${a.name}</span>
  2069. <span class="path t">${t.l1}›${t.name}</span>
  2070. <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>
  2071. <span class="tier tb${tb}">gemini·${tn}</span>
  2072. </div>
  2073. <div class="reason${s === 0 ? ' inv' : ''}">${esc(cell.r || '(模型未给该格评分)')}</div>
  2074. <div class="lbl">系统生成 · 知识类型=${st.lens}${st.tools.length ? ' · 工具类型=' + st.tools.join('/') : ''}</div>
  2075. <ul>` + gen.map(([fName, genQStr], idx) => {
  2076. const formKey = formKeys[idx];
  2077. const actualQ = dbFormMap[formKey];
  2078. const isCurrent = formKey === st.form;
  2079. let content = `<span class="fm">${fName}</span>${esc(genQStr)}`;
  2080. if (actualQ) {
  2081. if (actualQ !== genQStr) {
  2082. 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);">
  2083. <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>
  2084. <span style="word-break: break-all; font-family: inherit;">${esc(actualQ)}</span>
  2085. </div>`;
  2086. } else {
  2087. 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);">
  2088. <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>
  2089. <span style="font-style: italic; font-family: inherit; color: #6b7280;">(同上)</span>
  2090. </div>`;
  2091. }
  2092. }
  2093. return `<li class="gen${isCurrent ? ' cur' : ''}" style="display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 6px 8px;">${content}</li>`;
  2094. }).join('') + '</ul>';
  2095. pop.innerHTML = html;
  2096. pop.style.display = 'block';
  2097. pop.style.left = Math.max(8, Math.min(x, window.innerWidth - 466)) + 'px';
  2098. pop.style.top = Math.max(8, Math.min(y, window.innerHeight - pop.offsetHeight - 12)) + 'px';
  2099. pop.querySelector('.close').onclick = () => {
  2100. pop.style.display = 'none';
  2101. openCell = null;
  2102. };
  2103. }
  2104. function computeStats() {
  2105. const stat = { "0": 0, "1": 0, "2": 0, "3": 0, "na": 0 };
  2106. if (DATA.matrix) {
  2107. DATA.matrix.forEach(row => row.forEach(cell => {
  2108. const s = cell ? (cell.tier !== undefined ? cell.tier : cell.s) : null;
  2109. if (s === null || s === undefined) stat["na"]++;
  2110. else if (stat[s] !== undefined) stat[s]++;
  2111. }));
  2112. }
  2113. document.getElementById('s3').textContent = stat['3'];
  2114. document.getElementById('s2').textContent = stat['2'];
  2115. document.getElementById('s1').textContent = stat['1'];
  2116. document.getElementById('s0').textContent = stat['0'];
  2117. document.getElementById('sna').textContent = stat['na'];
  2118. }
  2119. function renderFormsChan() {
  2120. const q = st.qi === -1 ? null : DATA.queries[st.qi];
  2121. if (!q) {
  2122. document.getElementById("navC").innerHTML = '';
  2123. return;
  2124. }
  2125. const f = curForm();
  2126. if (!f) {
  2127. document.getElementById("navC").innerHTML = '';
  2128. return;
  2129. }
  2130. const isProcedureView = st.matrixView === 'procedures';
  2131. const resultsFilter = r => {
  2132. if (isProcedureView) {
  2133. return r.procedure_html && r.procedure_html !== "";
  2134. }
  2135. return true;
  2136. };
  2137. const filteredResults = f.results.filter(resultsFilter);
  2138. const chans = ["all", ...f.platforms];
  2139. document.getElementById("navC").innerHTML = chans.map(c => {
  2140. const n = c === "all" ? filteredResults.length : filteredResults.filter(r => r.platformKey === c).length;
  2141. return `<span class="tab ${c === st.channel ? 'on' : ''}" data-c="${c}">${c === "all" ? "全部" : (PLATC[c] || c)} <small>${n}</small></span>`;
  2142. }).join("");
  2143. }
  2144. function renderNav() { renderMatrix(); renderFormsChan(); }
  2145. function renderHead() {
  2146. const f = curForm();
  2147. if (!f) return;
  2148. const q = DATA.queries[st.qi];
  2149. if (q) {
  2150. 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>`;
  2151. } else {
  2152. document.getElementById("lede").textContent = "";
  2153. }
  2154. let it = f.results;
  2155. if (st.matrixView === 'procedures') {
  2156. it = it.filter(r => r.procedure_html && r.procedure_html !== "");
  2157. }
  2158. if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
  2159. const valid = it.filter(r => !r.anomaly);
  2160. const rep = valid.filter(r => !isItemDiscarded(r)).length;
  2161. const dis = valid.filter(r => isItemDiscarded(r)).length;
  2162. const anom = it.filter(r => r.anomaly).length;
  2163. const avg = (valid.reduce((s, r) => s + r.overall, 0) / (valid.length || 1)).toFixed(1);
  2164. const lab = st.channel === "all" ? "该形式结果数" : (PLATC[st.channel] || st.channel) + " 结果数";
  2165. 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("");
  2166. }
  2167. // POST /api/reeval —— 后台只对当前 query 的所有 form 文件复评(不重新搜索);
  2168. // server.py 立即返回 {status:'started', pid, log},前端开启轮询自动扫描刷新。
  2169. function reevalCurrentQuery() {
  2170. if (st.qi === -1 || !DATA.queries[st.qi]) { alert('请先选一个 query 再重评'); return; }
  2171. const q = DATA.queries[st.qi].key;
  2172. if (!confirm(`重评 ${q} 的所有帖子(A/B/C 三种 form)?\n约 1-3 分钟(视帖子数),过程中页面可继续浏览。\n完成后会自动重新扫描并更新页面。`)) return;
  2173. const btn = document.getElementById('reevalBtn');
  2174. btn.disabled = true; btn.textContent = '♻️ 提交中…';
  2175. fetch('/api/reeval', {
  2176. method: 'POST', headers: { 'Content-Type': 'application/json' },
  2177. body: JSON.stringify({ q }),
  2178. }).then(r => r.json().then(d => ({ ok: r.ok, d }))).then(({ ok, d }) => {
  2179. if (ok && d.status === 'started') {
  2180. if (!DATA.active_reevals) DATA.active_reevals = {};
  2181. DATA.active_reevals[q] = "running";
  2182. btn.textContent = `♻️ 重评中 ${q}...`;
  2183. startReevalPolling(q);
  2184. } else {
  2185. btn.disabled = false; btn.textContent = '♻️ 重评当前 query';
  2186. alert('启动失败:' + (d.error || JSON.stringify(d)));
  2187. }
  2188. }).catch(e => {
  2189. btn.disabled = false; btn.textContent = '♻️ 重评当前 query';
  2190. alert('请求失败:' + e);
  2191. });
  2192. }
  2193. function sortedItems() {
  2194. const f = curForm();
  2195. if (!f) return [];
  2196. let it = f.results.slice();
  2197. if (st.matrixView === 'procedures') {
  2198. it = it.filter(r => r.procedure_html && r.procedure_html !== "");
  2199. }
  2200. if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
  2201. const s = document.getElementById("sort").value;
  2202. const decWeight = r => (r.anomaly ? 2 : (isItemDiscarded(r) ? 1 : 0));
  2203. it.sort((a, b) => {
  2204. const wA = decWeight(a), wB = decWeight(b);
  2205. if (wA !== wB) return wA - wB;
  2206. if (s === "score") return b.overall - a.overall;
  2207. if (s === "date") return (b.date || "").localeCompare(b.date || "");
  2208. if (s === "platform") return (a.platform || "").localeCompare(b.platform || "", "zh-Hans") || b.overall - a.overall;
  2209. return 0;
  2210. });
  2211. return it;
  2212. }
  2213. function renderGrid() {
  2214. VIEW = sortedItems();
  2215. document.getElementById("grid").innerHTML = VIEW.map((it, idx) => {
  2216. 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}">`;
  2217. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  2218. let bars = "";
  2219. if (isNewSchema) {
  2220. const relAvg = groupAverage(it, scoreGroupsNew[0], true) || 0;
  2221. const qualAvg = getQualityAverage(it) || 0;
  2222. const newBars = [
  2223. { label: "相关性评分 (均分)", v: relAvg },
  2224. { label: "质量评分 (均分)", v: qualAvg }
  2225. ];
  2226. bars = newBars.map(b => {
  2227. const w = b.v * 10;
  2228. return `<span title="${esc(b.label)} ${b.v.toFixed(1)}"><span class="fill" style="width:${w}%; background: #2563eb;"></span></span>`;
  2229. }).join("");
  2230. } else {
  2231. const oldKeys = ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"];
  2232. bars = oldKeys.map(k => {
  2233. const val = it.scores[k] !== undefined ? parseFloat(it.scores[k]) : 0;
  2234. const w = val * 20;
  2235. return `<span title="${esc(commonLabels[k] || k)} ${val}"><span class="fill" style="width:${w}%;"></span></span>`;
  2236. }).join("");
  2237. }
  2238. const isDiscard = isItemDiscarded(it);
  2239. let discardReason = it.reason || '未提供过滤原因';
  2240. if (it.production_relevance !== null && it.production_relevance !== undefined) {
  2241. const input = document.getElementById("relThreshold");
  2242. const threshold = input ? parseFloat(input.value) : (isNewSchema ? 4.0 : 2.0);
  2243. const relVal = parseFloat(it.production_relevance);
  2244. if (!isNaN(relVal) && relVal < threshold) {
  2245. discardReason = `相关性得分 (${relVal}) 低于过滤阈值 (${threshold})`;
  2246. }
  2247. }
  2248. const discardOverlayHtml = isDiscard ? `
  2249. <div class="discard-overlay">
  2250. <div class="discard-badge">Discarded</div>
  2251. <div class="discard-reason">${esc(discardReason)}</div>
  2252. </div>
  2253. ` : '';
  2254. return `<article class="result ${isDiscard ? 'discard' : ''}">
  2255. ${discardOverlayHtml}
  2256. <div class="thumbs">${imgs}</div>
  2257. <div class="body">
  2258. <div class="meta"><span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>${esc(it.date)} · ${esc(it.engagement)}</span></div>
  2259. <h2>${esc(it.title)}</h2>
  2260. <div class="excerpt">${esc(it.text)}</div>
  2261. <div class="tags">${it.tools.slice(0, 4).map(t => `<span class="tag">${esc(t)}</span>`).join("")}</div>
  2262. <div class="scorebar">
  2263. <div class="overall">
  2264. <div>
  2265. <div class="score">${it.anomaly ? '—' : it.overall.toFixed(1)}</div>
  2266. <small>综合分</small>
  2267. </div>
  2268. <div class="decision ${it.anomaly ? '' : it.decision}">${it.anomaly ? '异常' : esc(it.decision)}</div>
  2269. </div>
  2270. <div class="mini-bars ${isNewSchema ? 'new-schema' : ''}">${bars}</div>
  2271. <div class="group-snapshot ${isNewSchema ? 'new-schema' : ''}">${groupSnapshot(it)}</div>
  2272. <div class="actions">
  2273. <button onclick="openDetail(${idx})">查看详情</button>
  2274. <a href="${esc(it.url)}" target="_blank" rel="noreferrer">原链接</a>
  2275. </div>
  2276. </div>
  2277. </div>
  2278. </article>`;
  2279. }).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>');
  2280. }
  2281. const ENGINE_MODELS = {
  2282. cyber_runner: [
  2283. { value: "google/gemini-3.1-flash-lite", text: "google/gemini-3.1-flash-lite (默认)" },
  2284. { value: "google/gemini-2.5-flash", text: "google/gemini-2.5-flash" },
  2285. { value: "openai/gpt-4o", text: "openai/gpt-4o" },
  2286. { value: "anthropic/claude-3.5-sonnet", text: "anthropic/claude-3.5-sonnet" }
  2287. ],
  2288. claude_sdk: [
  2289. { value: "claude-sonnet-4-6", text: "claude-sonnet-4-6 (默认)" },
  2290. { value: "claude-haiku-4-6", text: "claude-haiku-4-6" }
  2291. ]
  2292. };
  2293. function onProcEngineChange() {
  2294. const engine = document.getElementById("procEngineSelect").value;
  2295. const modelSelect = document.getElementById("procModelSelect");
  2296. modelSelect.innerHTML = ENGINE_MODELS[engine].map(m => `<option value="${m.value}">${m.text}</option>`).join("");
  2297. }
  2298. function getShortCaseFolder(form, case_id) {
  2299. const match = case_id.match(/^([a-z]+)_([0-9a-f]{8})/i);
  2300. const short = match ? `${match[1]}_${match[2]}` : case_id.substring(0, 20);
  2301. return `${form}_${short}`;
  2302. }
  2303. function checkProcedureState(it) {
  2304. if (procPollInterval) {
  2305. clearInterval(procPollInterval);
  2306. procPollInterval = null;
  2307. }
  2308. currentProcTask = {
  2309. q: it.run,
  2310. form: st.form,
  2311. case_id: it.case_id,
  2312. procedure_html: it.procedure_html
  2313. };
  2314. isLogViewActive = false;
  2315. document.getElementById("procStatusText").textContent = "正在检测工序状态...";
  2316. document.getElementById("procActionBtns").innerHTML = "";
  2317. document.getElementById("procSetupPanel").style.display = "none";
  2318. document.getElementById("procConsolePanel").style.display = "none";
  2319. document.getElementById("procedureIframe").style.display = "none";
  2320. const url = `/api/procedure_status?q=${it.run}&form=${st.form}&case_id=${it.case_id}`;
  2321. fetch(url)
  2322. .then(r => r.json())
  2323. .then(d => {
  2324. if (d.status === "running") {
  2325. showProcConsole("running");
  2326. pollProcLogs();
  2327. } else if (d.status === "success") {
  2328. showProcIframe(it.procedure_html);
  2329. } else if (d.status === "failed") {
  2330. showProcSetup(d.error);
  2331. } else {
  2332. showProcSetup();
  2333. }
  2334. })
  2335. .catch(err => {
  2336. showProcSetup("检查状态失败: " + err);
  2337. });
  2338. }
  2339. function showProcSetup(errorMsg) {
  2340. document.getElementById("procSetupPanel").style.display = "flex";
  2341. document.getElementById("procConsolePanel").style.display = "none";
  2342. document.getElementById("procedureIframe").style.display = "none";
  2343. const statusText = document.getElementById("procStatusText");
  2344. if (errorMsg) {
  2345. statusText.innerHTML = `<span style="color:var(--rose)">提取失败: ${esc(errorMsg)}</span>`;
  2346. } else {
  2347. statusText.textContent = "未生成工序";
  2348. }
  2349. document.getElementById("procActionBtns").innerHTML = "";
  2350. document.getElementById("startProcBtn").disabled = false;
  2351. document.getElementById("startProcBtn").textContent = "开始提取工序";
  2352. onProcEngineChange();
  2353. }
  2354. function showProcConsole(status) {
  2355. document.getElementById("procSetupPanel").style.display = "none";
  2356. document.getElementById("procConsolePanel").style.display = "flex";
  2357. document.getElementById("procedureIframe").style.display = "none";
  2358. const statusText = document.getElementById("procStatusText");
  2359. const consoleStatus = document.getElementById("procConsoleStatus");
  2360. if (status === "running") {
  2361. statusText.innerHTML = `<span>⏳ 正在提取工序中...</span>`;
  2362. consoleStatus.textContent = "running";
  2363. document.getElementById("procActionBtns").innerHTML = `<button class="btn" style="background:#f3f4f6" disabled>正在生成...</button>`;
  2364. } else if (status === "failed") {
  2365. statusText.innerHTML = `<span style="color:var(--rose)">❌ 提取失败</span>`;
  2366. consoleStatus.textContent = "failed";
  2367. document.getElementById("procActionBtns").innerHTML = `
  2368. <button class="btn" onclick="toggleCompletedLogs()">查看日志</button>
  2369. <button class="btn" style="background:var(--rose); color:#fff; border-color:var(--rose)" onclick="resetToSetup()">重试提取</button>
  2370. `;
  2371. }
  2372. }
  2373. function resetToSetup() {
  2374. showProcSetup();
  2375. }
  2376. function showProcIframe(htmlPath) {
  2377. document.getElementById("procSetupPanel").style.display = "none";
  2378. document.getElementById("procConsolePanel").style.display = "none";
  2379. const iframe = document.getElementById("procedureIframe");
  2380. iframe.style.display = "block";
  2381. iframe.src = "/" + htmlPath;
  2382. document.getElementById("procStatusText").innerHTML = `<span style="color:var(--mint)">✓ 工序已生成</span>`;
  2383. document.getElementById("procActionBtns").innerHTML = `
  2384. <button class="btn" id="btnToggleLogs" onclick="toggleCompletedLogs()">📋 查看提取日志</button>
  2385. <button class="btn" style="background:var(--rose); color:#fff; border-color:var(--rose);" onclick="regenerateProcedureConfirm()">♻️ 重新生成</button>
  2386. `;
  2387. }
  2388. function toggleCompletedLogs() {
  2389. const iframe = document.getElementById("procedureIframe");
  2390. const consolePanel = document.getElementById("procConsolePanel");
  2391. const toggleBtn = document.getElementById("btnToggleLogs");
  2392. if (isLogViewActive) {
  2393. consolePanel.style.display = "none";
  2394. iframe.style.display = "block";
  2395. if (toggleBtn) toggleBtn.textContent = "📋 查看提取日志";
  2396. isLogViewActive = false;
  2397. } else {
  2398. iframe.style.display = "none";
  2399. consolePanel.style.display = "flex";
  2400. if (toggleBtn) toggleBtn.textContent = "👁️ 返回工序效果";
  2401. isLogViewActive = true;
  2402. fetchLogsOnce();
  2403. }
  2404. }
  2405. function fetchLogsOnce() {
  2406. const consoleOutput = document.getElementById("procConsoleOutput");
  2407. consoleOutput.textContent = "正在加载日志...";
  2408. const url = `/api/procedure_log?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
  2409. fetch(url)
  2410. .then(r => r.json())
  2411. .then(d => {
  2412. consoleOutput.textContent = d.log || "没有提取日志。";
  2413. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2414. })
  2415. .catch(err => {
  2416. consoleOutput.textContent = "加载日志失败: " + err;
  2417. });
  2418. }
  2419. function startProcedureExtraction() {
  2420. const engine = document.getElementById("procEngineSelect").value;
  2421. const model = document.getElementById("procModelSelect").value;
  2422. const startBtn = document.getElementById("startProcBtn");
  2423. startBtn.disabled = true;
  2424. startBtn.textContent = "正在启动...";
  2425. const payload = {
  2426. q: currentProcTask.q,
  2427. form: currentProcTask.form,
  2428. case_id: currentProcTask.case_id,
  2429. engine: engine,
  2430. model: model
  2431. };
  2432. fetch("/api/generate_procedure", {
  2433. method: "POST",
  2434. headers: { "Content-Type": "application/json" },
  2435. body: JSON.stringify(payload)
  2436. })
  2437. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  2438. .then(({ ok, d }) => {
  2439. if (ok && d.status === "started") {
  2440. showProcConsole("running");
  2441. pollProcLogs();
  2442. } else {
  2443. showProcSetup(d.error || "启动失败");
  2444. }
  2445. })
  2446. .catch(err => {
  2447. showProcSetup("网络请求错误: " + err);
  2448. });
  2449. }
  2450. function regenerateProcedureConfirm() {
  2451. if (confirm("确定要重新提取并生成工序吗?这会覆盖原有的工序文件和修改记录。")) {
  2452. showProcSetup();
  2453. }
  2454. }
  2455. function pollProcLogs() {
  2456. if (procPollInterval) {
  2457. clearInterval(procPollInterval);
  2458. }
  2459. const consoleOutput = document.getElementById("procConsoleOutput");
  2460. consoleOutput.textContent = "正在连接后台提取进程...\n";
  2461. procPollInterval = setInterval(() => {
  2462. const statusUrl = `/api/procedure_status?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
  2463. fetch(statusUrl)
  2464. .then(r => r.json())
  2465. .then(d => {
  2466. if (d.status === "success") {
  2467. clearInterval(procPollInterval);
  2468. procPollInterval = null;
  2469. loadData(true);
  2470. if (d.procedure_html) {
  2471. showProcIframe(d.procedure_html);
  2472. } else {
  2473. const folder = getShortCaseFolder(currentProcTask.form, currentProcTask.case_id);
  2474. showProcIframe(`runs_full/${currentProcTask.q}/procedures/${folder}/case-${currentProcTask.case_id}.html`);
  2475. }
  2476. } else if (d.status === "failed") {
  2477. clearInterval(procPollInterval);
  2478. procPollInterval = null;
  2479. showProcConsole("failed");
  2480. }
  2481. });
  2482. const logUrl = `/api/procedure_log?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
  2483. fetch(logUrl)
  2484. .then(r => r.json())
  2485. .then(d => {
  2486. if (d.log) {
  2487. const scrollAtBottom = consoleOutput.scrollTop + consoleOutput.clientHeight >= consoleOutput.scrollHeight - 50;
  2488. consoleOutput.textContent = d.log;
  2489. if (scrollAtBottom) {
  2490. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2491. }
  2492. }
  2493. });
  2494. }, 2000);
  2495. }
  2496. function switchModalTab(tabName) {
  2497. const detailTab = document.getElementById("tabDetailBtn");
  2498. const procTab = document.getElementById("tabProcedureBtn");
  2499. const toolsTab = document.getElementById("tabToolsBtn");
  2500. const detailContent = document.getElementById("modalContentDetail");
  2501. const procContent = document.getElementById("modalContentProcedure");
  2502. const toolsContent = document.getElementById("modalContentTools");
  2503. // 全部复位
  2504. [detailTab, procTab, toolsTab].forEach(t => t && t.classList.remove("active"));
  2505. detailContent.style.display = "none";
  2506. procContent.style.display = "none";
  2507. if (toolsContent) toolsContent.style.display = "none";
  2508. if (tabName === 'procedure') {
  2509. procTab.classList.add("active");
  2510. procContent.style.display = "flex";
  2511. } else if (tabName === 'tools') {
  2512. toolsTab.classList.add("active");
  2513. toolsContent.style.display = "block";
  2514. loadToolsForCurrent(); // 切到工具 tab 时按需加载
  2515. } else {
  2516. detailTab.classList.add("active");
  2517. detailContent.style.display = "grid";
  2518. }
  2519. }
  2520. // fixed_query_eval:图片点击放大(灯箱用 dialog,叠在详情 modal 上层)
  2521. function openLightbox(src) {
  2522. const d = document.getElementById('imgLightbox');
  2523. document.getElementById('imgLightboxImg').src = src;
  2524. d.showModal();
  2525. }
  2526. // ════════ fixed_query_eval:工具解构(批量选帖 + 单帖 + 结果渲染)════════
  2527. const TOOL_MODEL_LABEL = 'gemini-3.1-flash-lite';
  2528. function _curQueryPosts() {
  2529. const q = DATA.queries[st.qi];
  2530. if (!q) return { key: null, label: '', items: [] };
  2531. const form = q.forms && q.forms[st.fi];
  2532. let items = (form && form.results) || [];
  2533. if (st.channel !== 'all') items = items.filter(r => r.platformKey === st.channel);
  2534. return { key: q.key, label: q.original_q || q.key, items };
  2535. }
  2536. function openToolBatchModal() {
  2537. const { key, label, items } = _curQueryPosts();
  2538. if (!key) { alert('请先选择一个 query'); return; }
  2539. document.getElementById('toolBatchSub').textContent =
  2540. `${label} · ${st.channel === 'all' ? '全部渠道' : (PLATC[st.channel] || st.channel)} · 共 ${items.length} 帖`;
  2541. document.getElementById('toolBatchAll').checked = false;
  2542. document.getElementById('toolBatchList').innerHTML = items.map(it => {
  2543. const img = (it.images && it.images[0]) || '';
  2544. return `<label style="display:flex; gap:10px; align-items:center; padding:8px 4px; border-bottom:1px solid var(--line); cursor:pointer;">
  2545. <input type="checkbox" class="tb-item" value="${esc(it.case_id)}" onchange="toolBatchUpdateCount()">
  2546. ${img ? `<img src="${esc(img)}" referrerpolicy="no-referrer" style="width:42px;height:42px;object-fit:cover;border-radius:6px;flex-shrink:0;" onerror="this.style.opacity=.2">`
  2547. : '<div style="width:42px;height:42px;background:#f1f1f1;border-radius:6px;flex-shrink:0;"></div>'}
  2548. <span class="platform p-${esc(it.platformKey)}" style="flex-shrink:0">${esc(it.platform)}</span>
  2549. <span style="flex:1; font-size:13px; line-height:1.4; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;">${esc(it.title || '(无标题)')}</span>
  2550. </label>`;
  2551. }).join('') || '<p style="color:var(--muted); padding:24px; text-align:center;">该渠道暂无帖子</p>';
  2552. toolBatchUpdateCount();
  2553. toolBatchDialog.showModal();
  2554. }
  2555. function toolBatchToggleAll(on) {
  2556. document.querySelectorAll('#toolBatchList .tb-item').forEach(c => c.checked = on);
  2557. toolBatchUpdateCount();
  2558. }
  2559. function toolBatchUpdateCount() {
  2560. const n = document.querySelectorAll('#toolBatchList .tb-item:checked').length;
  2561. document.getElementById('toolBatchCount').textContent = `已选 ${n} 帖`;
  2562. }
  2563. function confirmToolExtract() {
  2564. const ids = [...document.querySelectorAll('#toolBatchList .tb-item:checked')].map(c => c.value);
  2565. if (!ids.length) { alert('请至少选择一个帖子'); return; }
  2566. const { key } = _curQueryPosts();
  2567. const btn = document.getElementById('toolBatchConfirm');
  2568. btn.disabled = true; btn.textContent = '启动中…';
  2569. fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
  2570. body: JSON.stringify({ q: key, case_ids: ids }) })
  2571. .then(r => r.json()).then(d => {
  2572. btn.disabled = false; btn.textContent = '确认解构';
  2573. if (d.status === 'started') {
  2574. alert(`已开始解构 ${d.count} 帖(${TOOL_MODEL_LABEL})。\n完成后打开帖子详情的「工具解构」tab 查看。`);
  2575. toolBatchDialog.close();
  2576. } else { alert('启动失败:' + (d.error || '未知')); }
  2577. }).catch(e => { btn.disabled = false; btn.textContent = '确认解构'; alert('请求失败:' + e); });
  2578. }
  2579. // ── 详情页「工具解构」tab ──
  2580. function _curDetailItem() { return VIEW[detailDialog.dataset.activeIdx]; }
  2581. function loadToolsForCurrent() {
  2582. const it = _curDetailItem(); if (!it) return;
  2583. const pane = document.getElementById('modalContentTools');
  2584. pane.innerHTML = '<div style="padding:40px;text-align:center;color:var(--muted)">加载中…</div>';
  2585. fetch(`/api/tools_data?q=${encodeURIComponent(it.run)}&case_id=${encodeURIComponent(it.case_id)}`)
  2586. .then(r => r.json()).then(d => { pane.innerHTML = d.exists ? renderToolCards(d) : toolSetupHTML(); })
  2587. .catch(() => { pane.innerHTML = '<div style="padding:40px;color:#c0392b">加载失败</div>'; });
  2588. }
  2589. function toolSetupHTML() {
  2590. return `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 20px;">
  2591. <div style="font-size:36px;margin-bottom:14px;">🔧</div>
  2592. <h4 style="margin:0 0 8px;font-size:18px;">解构本帖工具</h4>
  2593. <p style="color:var(--muted);font-size:14px;max-width:480px;margin:0 0 20px;">该帖子尚未解构工具信息。将用 <b>${TOOL_MODEL_LABEL}</b> 读取正文 + 配图,提炼每个工具的结构化条目。</p>
  2594. <button onclick="startToolExtractSingle()" style="background:var(--mint);color:#fff;border-color:var(--mint);padding:10px 24px;font-size:15px;font-weight:bold;border-radius:8px;cursor:pointer;">开始解构</button>
  2595. </div>`;
  2596. }
  2597. function _pollToolsThenLoad(it, pane) {
  2598. const timer = setInterval(() => {
  2599. fetch(`/api/tools_status?q=${encodeURIComponent(it.run)}&case_id=${encodeURIComponent(it.case_id)}`)
  2600. .then(r => r.json()).then(s => {
  2601. if (!s.running) {
  2602. clearInterval(timer);
  2603. if (s.error) pane.innerHTML = `<div style="padding:40px;color:#c0392b">解构失败:${esc(s.error)}</div>`;
  2604. else loadToolsForCurrent();
  2605. }
  2606. }).catch(() => {});
  2607. }, 2500);
  2608. }
  2609. function startToolExtractSingle() {
  2610. const it = _curDetailItem(); if (!it) return;
  2611. const pane = document.getElementById('modalContentTools');
  2612. pane.innerHTML = `<div style="padding:48px;text-align:center;color:var(--muted)">⏳ 正在解构本帖工具…(${TOOL_MODEL_LABEL})</div>`;
  2613. fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
  2614. body: JSON.stringify({ q: it.run, case_ids: [it.case_id] }) })
  2615. .then(r => r.json()).then(d => {
  2616. if (d.status !== 'started') { pane.innerHTML = `<div style="padding:40px;color:#c0392b">启动失败:${esc(d.error || '未知')}</div>`; return; }
  2617. _pollToolsThenLoad(it, pane);
  2618. });
  2619. }
  2620. function reExtractTools() {
  2621. const it = _curDetailItem(); if (!it) return;
  2622. if (!confirm('重新解构会覆盖已有结果,确定?')) return;
  2623. const pane = document.getElementById('modalContentTools');
  2624. pane.innerHTML = `<div style="padding:48px;text-align:center;color:var(--muted)">⏳ 重新解构中…(${TOOL_MODEL_LABEL})</div>`;
  2625. fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
  2626. body: JSON.stringify({ q: it.run, case_ids: [it.case_id], force: true }) })
  2627. .then(r => r.json()).then(() => _pollToolsThenLoad(it, pane));
  2628. }
  2629. function _toolField(label, val, color) {
  2630. if (val === null || val === undefined || val === '' || (Array.isArray(val) && !val.length)) return '';
  2631. const body = Array.isArray(val)
  2632. ? '<ul style="margin:2px 0 0;padding-left:18px;">' + val.map(x => `<li style="margin:2px 0;">${esc(String(x))}</li>`).join('') + '</ul>'
  2633. : `<span>${esc(String(val))}</span>`;
  2634. return `<div style="margin-top:8px;font-size:13px;line-height:1.5;"><span style="font-size:12px;font-weight:700;color:${color || 'var(--muted)'};">${label}</span> ${body}</div>`;
  2635. }
  2636. let toolViewMode = 'cards'; // 'cards' | 'table'
  2637. let _lastToolsData = null;
  2638. function renderToolCards(d) {
  2639. _lastToolsData = d;
  2640. const tools = d.tools || [];
  2641. const toggleLabel = toolViewMode === 'table' ? '▤ 卡片视图' : '⊞ 表格视图';
  2642. const header = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
  2643. <div style="font-size:14px;color:var(--muted);">共 <b style="color:var(--ink);">${tools.length}</b> 个工具 · 模型 ${esc(d.model || TOOL_MODEL_LABEL)}</div>
  2644. <div style="display:flex;gap:8px;">
  2645. <button onclick="toggleToolView()" style="font-size:12px;">${toggleLabel}</button>
  2646. <button onclick="reExtractTools()" style="font-size:12px;">↻ 重新解构</button>
  2647. </div>
  2648. </div>`;
  2649. if (!tools.length) return header + '<p style="color:var(--muted);padding:30px;text-align:center;">未识别到工具。</p>';
  2650. return header + (toolViewMode === 'table' ? renderToolTable(tools) : renderToolCardsBody(tools));
  2651. }
  2652. function toggleToolView() {
  2653. toolViewMode = toolViewMode === 'table' ? 'cards' : 'table';
  2654. if (_lastToolsData) document.getElementById('modalContentTools').innerHTML = renderToolCards(_lastToolsData);
  2655. }
  2656. function renderToolCardsBody(tools) {
  2657. return tools.map(t => {
  2658. const layer = t['创作层级'] || '';
  2659. const lc = layer === '制作层' ? '#0e7490' : (layer === '创作层' ? '#b87918' : '#9aa0ad');
  2660. const lbg = layer === '制作层' ? '#e0f2f1' : (layer === '创作层' ? '#fef3e2' : '#f1f1f1');
  2661. const tags = [];
  2662. if (t['实质作用域']) tags.push(`<span class="tag">实质:${esc(t['实质作用域'])}</span>`);
  2663. if (t['形式作用域']) tags.push(`<span class="tag">形式:${esc(t['形式作用域'])}</span>`);
  2664. const updated = t['最新更新时间'] ? `<span style="font-size:12px;color:var(--muted);">⏱ ${esc(t['最新更新时间'])}</span>` : '';
  2665. const link = t['来源链接'] ? `<div style="margin-top:8px;"><a href="${esc(t['来源链接'])}" target="_blank" style="font-size:13px;color:#2563eb;">🔗 来源链接</a></div>` : '';
  2666. return `<div style="border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:12px;background:#fff;box-shadow:var(--shadow);">
  2667. <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
  2668. <h4 style="margin:0;font-size:16px;">🔧 ${esc(t['工具名称'] || '(未命名)')}</h4>
  2669. ${layer ? `<span style="font-size:12px;font-weight:700;color:${lc};background:${lbg};padding:2px 10px;border-radius:20px;flex-shrink:0;">${esc(layer)}</span>` : ''}
  2670. </div>
  2671. ${(tags.length || updated) ? `<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-top:8px;">${tags.join('')} ${updated}</div>` : ''}
  2672. <div style="display:flex;gap:28px;flex-wrap:wrap;">${_toolField('输入', t['输入'])}${_toolField('输出', t['输出'])}</div>
  2673. ${_toolField('用法', t['用法'])}
  2674. ${_toolField('案例', t['案例'])}
  2675. ${_toolField('缺点', t['缺点'], '#b8651a')}
  2676. ${link}
  2677. </div>`;
  2678. }).join('');
  2679. }
  2680. // 表格视图:列对应数据库 fqe_tools 字段
  2681. function _toolCell(v) {
  2682. if (v === null || v === undefined || v === '' || (Array.isArray(v) && !v.length)) return '<span style="color:#c4c4c4">—</span>';
  2683. if (Array.isArray(v)) return '<ul style="margin:0;padding-left:15px;">' + v.map(x => `<li style="margin:1px 0;">${esc(String(x))}</li>`).join('') + '</ul>';
  2684. return esc(String(v));
  2685. }
  2686. function renderToolTable(tools) {
  2687. const cols = ['工具名称', '创作层级', '实质作用域', '形式作用域', '输入', '输出', '用法', '案例', '缺点', '来源链接', '最新更新时间'];
  2688. const tdBase = 'border:1px solid var(--line);padding:7px 9px;vertical-align:top;font-size:12px;line-height:1.5;';
  2689. const th = cols.map(c =>
  2690. `<th style="${tdBase}background:#faf7f1;font-weight:700;white-space:nowrap;position:sticky;top:0;z-index:1;">${c}</th>`
  2691. ).join('');
  2692. const rows = tools.map(t => {
  2693. const tds = cols.map(c => {
  2694. let inner;
  2695. if (c === '来源链接') {
  2696. inner = t[c] ? `<a href="${esc(t[c])}" target="_blank" style="color:#2563eb;">🔗 打开</a>` : '<span style="color:#c4c4c4">—</span>';
  2697. } else if (c === '创作层级' && t[c]) {
  2698. const lc = t[c] === '制作层' ? '#0e7490' : '#b87918';
  2699. const lbg = t[c] === '制作层' ? '#e0f2f1' : '#fef3e2';
  2700. inner = `<span style="font-weight:700;color:${lc};background:${lbg};padding:1px 8px;border-radius:12px;white-space:nowrap;">${esc(t[c])}</span>`;
  2701. } else {
  2702. inner = _toolCell(t[c]);
  2703. }
  2704. const mw = ['用法', '案例', '缺点', '输入', '输出'].includes(c) ? 'min-width:140px;' : (c === '工具名称' ? 'min-width:100px;font-weight:600;white-space:nowrap;' : 'white-space:nowrap;');
  2705. return `<td style="${tdBase}${mw}">${inner}</td>`;
  2706. }).join('');
  2707. return `<tr>${tds}</tr>`;
  2708. }).join('');
  2709. return `<div style="overflow-x:auto;border:1px solid var(--line);border-radius:8px;">
  2710. <table style="border-collapse:collapse;width:100%;min-width:900px;background:#fff;">
  2711. <thead><tr>${th}</tr></thead><tbody>${rows}</tbody>
  2712. </table></div>`;
  2713. }
  2714. function openDetail(i) {
  2715. const it = VIEW[i];
  2716. detailDialog.dataset.activeIdx = i;
  2717. currentPinnedScoreEl = null;
  2718. 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>`;
  2719. document.getElementById("modalTitle").textContent = it.title;
  2720. document.getElementById("modalReason").textContent = it.reason;
  2721. document.getElementById("modalText").textContent = it.text || "(无正文)";
  2722. document.getElementById("modalImages").innerHTML = it.images.length ? it.images.map(s => `<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" style="cursor:zoom-in" onclick="openLightbox(this.src)" onerror="this.style.opacity=.3">`).join("") : "<p>搜索详情未返回图片。</p>";
  2723. 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("");
  2724. const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
  2725. const maxScore = isNewSchema ? '10' : '5';
  2726. const overallVal = it.anomaly ? '—' : it.overall.toFixed(1);
  2727. const scoreColor = isNewSchema ? '#2563eb' : 'var(--mint)';
  2728. const scoreStrong = document.getElementById("modalOverallScoreVal");
  2729. if (scoreStrong) {
  2730. scoreStrong.style.color = scoreColor;
  2731. scoreStrong.innerHTML = `${overallVal}<span style="font-size: 12px; color: var(--muted); font-weight: 500; margin-left: 2px;">/${maxScore}</span>`;
  2732. }
  2733. if (isNewSchema) {
  2734. document.getElementById("modalScores").innerHTML = renderNewScores(it);
  2735. } else {
  2736. document.getElementById("modalScores").innerHTML = scoreGroupsOld.map(g => renderScoreGroup(it, g)).join("");
  2737. }
  2738. const tabs = document.getElementById("modalTabs");
  2739. tabs.style.display = "flex";
  2740. checkProcedureState(it);
  2741. switchModalTab('detail');
  2742. detailDialog.showModal();
  2743. }
  2744. let currentPinnedScoreEl = null;
  2745. function pinScoreReason(el, label, k) {
  2746. const activeIdx = detailDialog.dataset.activeIdx;
  2747. const it = VIEW[activeIdx];
  2748. if (!it || !it.score_reasons) return;
  2749. const reason = it.score_reasons[k] || '';
  2750. let tip = document.getElementById('scoreTip');
  2751. if (!tip) {
  2752. tip = document.createElement('div');
  2753. tip.id = 'scoreTip';
  2754. 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;';
  2755. detailDialog.appendChild(tip);
  2756. }
  2757. if (currentPinnedScoreEl === el && tip.style.display === 'block') {
  2758. tip.style.display = 'none';
  2759. el.style.color = '';
  2760. currentPinnedScoreEl = null;
  2761. } else {
  2762. if (currentPinnedScoreEl) {
  2763. currentPinnedScoreEl.style.color = '';
  2764. }
  2765. tip.innerHTML = `<strong style="display:block;margin-bottom:6px;color:#b87918;">【指标判定 - ${label}】</strong>${esc(reason)}`;
  2766. tip.style.display = 'block';
  2767. el.style.color = 'var(--amber)';
  2768. currentPinnedScoreEl = el;
  2769. const elRect = el.getBoundingClientRect();
  2770. const tipWidth = tip.offsetWidth || 280;
  2771. const tipHeight = tip.offsetHeight || 80;
  2772. let left = elRect.left + elRect.width / 2 - tipWidth / 2;
  2773. let top = elRect.bottom + 8;
  2774. if (left < 10) left = 10;
  2775. if (left + tipWidth > window.innerWidth - 10) {
  2776. left = window.innerWidth - tipWidth - 10;
  2777. }
  2778. if (top + tipHeight > window.innerHeight - 10) {
  2779. top = elRect.top - tipHeight - 8;
  2780. }
  2781. tip.style.left = left + 'px';
  2782. tip.style.top = top + 'px';
  2783. }
  2784. }
  2785. // Automatically close tooltip and clear interval when modal is closed
  2786. document.getElementById('detailDialog').addEventListener('close', () => {
  2787. const tip = document.getElementById('scoreTip');
  2788. if (tip) {
  2789. tip.style.display = 'none';
  2790. }
  2791. if (currentPinnedScoreEl) {
  2792. currentPinnedScoreEl.style.color = '';
  2793. currentPinnedScoreEl = null;
  2794. }
  2795. if (procPollInterval) {
  2796. clearInterval(procPollInterval);
  2797. procPollInterval = null;
  2798. }
  2799. });
  2800. // Close tooltip when dialog scrolls
  2801. document.getElementById('detailDialog').addEventListener('scroll', () => {
  2802. const tip = document.getElementById('scoreTip');
  2803. if (tip) {
  2804. tip.style.display = 'none';
  2805. }
  2806. if (currentPinnedScoreEl) {
  2807. currentPinnedScoreEl.style.color = '';
  2808. currentPinnedScoreEl = null;
  2809. }
  2810. }, true);
  2811. const KTM = { procedure: "工序", step: "步骤", tool: "工具" };
  2812. function rerender(mxClick) {
  2813. updateThresholdLimits();
  2814. if (st.qi === -1) {
  2815. document.getElementById("stats").innerHTML = "";
  2816. document.getElementById("lede").textContent = "未找到对应的数据库 Query,请尝试切换筛选组合。";
  2817. renderFormsChan();
  2818. renderGrid();
  2819. if (!mxClick) {
  2820. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  2821. }
  2822. return;
  2823. }
  2824. if (st.qi >= DATA.queries.length) st.qi = 0;
  2825. if (st.fi >= DATA.queries[st.qi].forms.length) st.fi = 0;
  2826. if (!mxClick) {
  2827. renderMatrix();
  2828. }
  2829. renderFormsChan();
  2830. renderHead();
  2831. renderGrid();
  2832. // Sync reevalBtn state
  2833. const reevalBtn = document.getElementById('reevalBtn');
  2834. if (reevalBtn && DATA.queries[st.qi]) {
  2835. const q = DATA.queries[st.qi].key;
  2836. const status = DATA.active_reevals && DATA.active_reevals[q];
  2837. if (status === "running") {
  2838. reevalBtn.disabled = true;
  2839. reevalBtn.textContent = `♻️ 重评中 ${q}...`;
  2840. startReevalPolling(q);
  2841. } else {
  2842. reevalBtn.disabled = false;
  2843. reevalBtn.textContent = '♻️ 重评当前 query';
  2844. }
  2845. }
  2846. }
  2847. function setMatrixView(mode) {
  2848. st.matrixView = mode;
  2849. document.querySelectorAll('.g-mxview .btn').forEach(btn => {
  2850. btn.classList.remove('on');
  2851. });
  2852. if (mode === 'full') document.getElementById('btnMxFull').classList.add('on');
  2853. else if (mode === 'hits') document.getElementById('btnMxHits').classList.add('on');
  2854. else if (mode === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
  2855. if (st.selectedAction && st.selectedType) {
  2856. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2857. }
  2858. renderMatrix();
  2859. if (openCell) {
  2860. const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
  2861. const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
  2862. if (newCell) {
  2863. showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
  2864. }
  2865. }
  2866. rerender(true);
  2867. }
  2868. function loadData(keep) {
  2869. fetch("/api/data").then(r => r.json()).then(d => {
  2870. DATA = d;
  2871. if (!keep) {
  2872. st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full', selectedAction: null, selectedType: null };
  2873. if (DATA.queries.length > 0) {
  2874. const firstQ = DATA.queries[0];
  2875. if (firstQ.dims) {
  2876. st.selectedAction = firstQ.dims.action;
  2877. st.selectedType = firstQ.dims.type;
  2878. st.lens = detectLens(firstQ);
  2879. }
  2880. }
  2881. }
  2882. // Dynamically populate tool type buttons
  2883. document.getElementById('toolBtns').innerHTML = TOOL_TYPES.map(t => `<button class="btn" data-k="tool" data-v="${t}">${t}</button>`).join('');
  2884. // Synchronize buttons states to st
  2885. document.querySelectorAll('.ctl .btn').forEach(btn => {
  2886. const k = btn.dataset.k, v = btn.dataset.v;
  2887. if (MULTI[k]) {
  2888. const ak = MULTI[k];
  2889. btn.classList.toggle('on', v === '' ? st[ak].length === 0 : st[ak].includes(v));
  2890. } else {
  2891. btn.classList.toggle('on', st[k] === v);
  2892. }
  2893. });
  2894. document.querySelectorAll('.g-mxview .btn').forEach(btn => {
  2895. btn.classList.remove('on');
  2896. });
  2897. const mv = st.matrixView || 'full';
  2898. if (mv === 'full') document.getElementById('btnMxFull').classList.add('on');
  2899. else if (mv === 'hits') document.getElementById('btnMxHits').classList.add('on');
  2900. else if (mv === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
  2901. computeStats();
  2902. if (st.selectedAction && st.selectedType) {
  2903. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2904. }
  2905. rerender();
  2906. });
  2907. }
  2908. // Matrix click listener (links matrix select to database select + opens pop-up)
  2909. document.getElementById("comboMx").addEventListener("click", e => {
  2910. const td = e.target.closest("td.cell");
  2911. if (!td) return;
  2912. const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
  2913. st.selectedAction = a.name;
  2914. st.selectedType = t.name;
  2915. showPop(td, e.clientX, e.clientY);
  2916. // Select the query
  2917. selectQueryByActiveCellAndControls(a.name, t.name);
  2918. document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
  2919. td.classList.add('sel');
  2920. applyCrosshair();
  2921. rerender(true); // mxClick = true, updates results without redrawing full table
  2922. });
  2923. // Header controls event delegation
  2924. const MULTI = { tool: 'tools' };
  2925. document.querySelector('.ctl').addEventListener('click', e => {
  2926. const b = e.target.closest('.btn');
  2927. if (!b) return;
  2928. const k = b.dataset.k, v = b.dataset.v, grp = document.querySelectorAll(`.ctl .btn[data-k="${k}"]`);
  2929. if (MULTI[k]) {
  2930. const ak = MULTI[k];
  2931. if (v === '') {
  2932. st[ak] = [];
  2933. } else {
  2934. if (st[ak].includes(v)) {
  2935. st[ak] = [];
  2936. } else {
  2937. st[ak] = [v];
  2938. }
  2939. }
  2940. grp.forEach(x => {
  2941. const xv = x.dataset.v;
  2942. x.classList.toggle('on', xv === '' ? st[ak].length === 0 : st[ak].includes(xv));
  2943. });
  2944. } else {
  2945. grp.forEach(x => x.classList.remove('on'));
  2946. b.classList.add('on');
  2947. st[k] = v;
  2948. }
  2949. if (st.selectedAction && st.selectedType) {
  2950. selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
  2951. }
  2952. renderMatrix();
  2953. if (openCell) {
  2954. const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
  2955. const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
  2956. if (newCell) {
  2957. showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
  2958. }
  2959. }
  2960. rerender(true);
  2961. });
  2962. // Close popups on clicking outside
  2963. document.addEventListener('click', e => {
  2964. if (!e.target.closest('td.cell') && !e.target.closest('.pop') && !e.target.closest('.btn')) {
  2965. pop.style.display = 'none';
  2966. openCell = null;
  2967. }
  2968. const tip = document.getElementById('scoreTip');
  2969. if (tip && !e.target.closest('.info-icon') && !e.target.closest('#scoreTip')) {
  2970. tip.style.display = 'none';
  2971. if (currentPinnedScoreEl) {
  2972. currentPinnedScoreEl.style.color = '';
  2973. currentPinnedScoreEl = null;
  2974. }
  2975. }
  2976. });
  2977. document.getElementById("navC").addEventListener("click", e => { const t = e.target.closest(".tab"); if (!t) return; st.channel = t.dataset.c; renderNav(); renderHead(); renderGrid(); });
  2978. document.getElementById("sort").addEventListener("change", renderGrid);
  2979. // ── fixed_query_eval:固定 query 选择条(替代正交矩阵导航)─────────────────────
  2980. function renderNavQ() {
  2981. const el = document.getElementById('navQ'); if (!el) return;
  2982. const qs = (DATA && DATA.queries) || [];
  2983. el.innerHTML = qs.length
  2984. ? qs.map((q, i) => `<span class="tab ${i === st.qi ? 'on' : ''}" data-qi="${i}">${q.original_q || q.key} <small>${q.tot || 0}</small></span>`).join('')
  2985. : '<span style="color:var(--muted)">runs_full/ 暂无数据,先跑 run_search.py</span>';
  2986. }
  2987. document.getElementById('navQ').addEventListener('click', e => {
  2988. const t = e.target.closest('.tab'); if (!t) return;
  2989. st.qi = +t.dataset.qi; st.fi = 0; st.form = 'A'; st.channel = 'all';
  2990. rerender(true); renderNavQ();
  2991. });
  2992. // 包一层 rerender:每次渲染后同步刷新 query 选择条高亮
  2993. const _origRerender = rerender;
  2994. rerender = function () { const r = _origRerender.apply(this, arguments); renderNavQ(); return r; };
  2995. loadData();
  2996. </script>
  2997. </body>
  2998. </html>