new_query.html 94 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710
  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. <meta name="referrer" content="no-referrer">
  7. <title>新 Query 搜索与评估 · 案例总览</title>
  8. <style>
  9. :root {
  10. --ink: #24211d;
  11. --muted: #6f6961;
  12. --line: #ded8ce;
  13. --paper: #fbfaf7;
  14. --panel: #ffffff;
  15. --mint: #1f8a70;
  16. --rose: #b24b63;
  17. --amber: #b87918;
  18. --cyan: #2a6f8f;
  19. --soft-mint: #e9f5f0;
  20. --soft-rose: #f8e9ee;
  21. --soft-amber: #fff2d9;
  22. --soft-cyan: #e7f2f7;
  23. --shadow: 0 18px 45px rgba(41, 35, 28, .08);
  24. }
  25. * {
  26. box-sizing: border-box;
  27. }
  28. body {
  29. margin: 0;
  30. color: var(--ink);
  31. background: var(--paper);
  32. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  33. line-height: 1.55;
  34. }
  35. header {
  36. padding: 30px 24px 18px;
  37. border-bottom: 1px solid var(--line);
  38. background: linear-gradient(180deg, #fff 0%, #fbfaf7 100%);
  39. }
  40. .wrap {
  41. max-width: 1440px;
  42. margin: 0 auto;
  43. }
  44. .eyebrow {
  45. display: flex;
  46. gap: 8px;
  47. align-items: center;
  48. color: var(--mint);
  49. font-size: 13px;
  50. font-weight: 700;
  51. text-transform: uppercase;
  52. letter-spacing: 0;
  53. }
  54. h1 {
  55. margin: 8px 0 4px;
  56. font-size: clamp(24px, 3.5vw, 36px);
  57. line-height: 1.1;
  58. letter-spacing: 0;
  59. }
  60. .lede {
  61. color: var(--muted);
  62. font-size: 14.5px;
  63. }
  64. /* Layout for two-column structure */
  65. .app-container {
  66. max-width: 1440px;
  67. margin: 0 auto;
  68. padding: 24px;
  69. display: grid;
  70. grid-template-columns: 320px 1fr;
  71. gap: 24px;
  72. align-items: start;
  73. }
  74. @media (max-width: 1024px) {
  75. .app-container {
  76. grid-template-columns: 1fr;
  77. }
  78. }
  79. /* Sidebar Styles */
  80. .sidebar {
  81. background: var(--panel);
  82. border: 1px solid var(--line);
  83. border-radius: 12px;
  84. padding: 20px;
  85. box-shadow: var(--shadow);
  86. height: fit-content;
  87. max-height: calc(100vh - 180px);
  88. overflow-y: auto;
  89. position: sticky;
  90. top: 24px;
  91. }
  92. .sidebar h3 {
  93. margin-top: 0;
  94. font-size: 15px;
  95. font-weight: 700;
  96. border-bottom: 1px solid var(--line);
  97. padding-bottom: 10px;
  98. display: flex;
  99. justify-content: space-between;
  100. align-items: center;
  101. }
  102. .sidebar-list {
  103. display: flex;
  104. flex-direction: column;
  105. gap: 8px;
  106. margin-top: 12px;
  107. }
  108. .sidebar-item {
  109. border: 1px solid var(--line);
  110. border-radius: 8px;
  111. padding: 10px 12px;
  112. background: #fff;
  113. cursor: pointer;
  114. transition: all 0.15s ease;
  115. font-size: 13px;
  116. font-weight: 500;
  117. display: flex;
  118. flex-direction: column;
  119. gap: 4px;
  120. }
  121. .sidebar-item:hover {
  122. border-color: #bbb;
  123. background: #fafaf9;
  124. }
  125. .sidebar-item.active {
  126. border-color: var(--mint);
  127. background: var(--soft-mint);
  128. color: var(--ink);
  129. font-weight: 700;
  130. box-shadow: inset 3px 0 0 var(--mint);
  131. }
  132. .sidebar-item .q-text {
  133. word-break: break-all;
  134. line-height: 1.35;
  135. }
  136. .sidebar-item .meta {
  137. font-size: 11px;
  138. color: var(--muted);
  139. display: flex;
  140. justify-content: space-between;
  141. margin-top: 2px;
  142. }
  143. .sidebar-item .meta strong {
  144. color: var(--mint);
  145. }
  146. /* Builder Card styling */
  147. .builder-card {
  148. background: var(--panel);
  149. border: 1px solid var(--line);
  150. border-radius: 12px;
  151. padding: 20px;
  152. box-shadow: var(--shadow);
  153. }
  154. .builder-card h3 {
  155. margin-top: 0;
  156. margin-bottom: 16px;
  157. font-size: 15px;
  158. font-weight: 700;
  159. border-bottom: 1px solid var(--line);
  160. padding-bottom: 10px;
  161. }
  162. .dimensions {
  163. display: flex;
  164. flex-direction: column;
  165. }
  166. .dim-group {
  167. padding: 4px 0;
  168. border-bottom: 1px solid #f2f2f2;
  169. }
  170. .dim-group:last-child {
  171. border-bottom: none;
  172. }
  173. .level-row {
  174. display: flex;
  175. align-items: flex-start;
  176. padding: 7px 0;
  177. gap: 10px;
  178. }
  179. .level-row.indent {
  180. margin-left: 78px;
  181. }
  182. .dim-label {
  183. font-size: 12.5px;
  184. color: #999;
  185. min-width: 68px;
  186. padding-top: 6px;
  187. flex-shrink: 0;
  188. display: flex;
  189. align-items: center;
  190. gap: 3px;
  191. }
  192. .dim-label::before {
  193. content: '·';
  194. color: #bbb;
  195. }
  196. .sub-label {
  197. font-size: 11.5px;
  198. color: #bbb;
  199. min-width: 56px;
  200. padding-top: 6px;
  201. flex-shrink: 0;
  202. }
  203. .chips-wrap {
  204. display: flex;
  205. flex-wrap: wrap;
  206. gap: 7px;
  207. align-items: center;
  208. flex: 1;
  209. }
  210. .chip {
  211. padding: 4px 12px;
  212. border-radius: 20px;
  213. font-size: 12.5px;
  214. cursor: pointer;
  215. border: 1.5px solid #e2e2e2;
  216. background: #fff;
  217. color: #666;
  218. transition: all .12s;
  219. user-select: none;
  220. white-space: nowrap;
  221. line-height: 1.4;
  222. }
  223. .chip:hover {
  224. border-color: #c0c0c0;
  225. color: #333;
  226. }
  227. .chip.none-active {
  228. background: #1a1a1a;
  229. border-color: #1a1a1a;
  230. color: #fff;
  231. font-weight: 500;
  232. }
  233. .chip.none-active:hover {
  234. background: #333;
  235. border-color: #333;
  236. }
  237. .chip.selected {
  238. background: var(--mint);
  239. border-color: var(--mint);
  240. color: #fff;
  241. font-weight: 500;
  242. }
  243. .chip.selected:hover {
  244. opacity: 0.9;
  245. }
  246. .placeholder-text {
  247. font-size: 12.5px;
  248. color: #ccc;
  249. font-style: italic;
  250. padding: 6px 0;
  251. }
  252. .preview-section {
  253. margin-top: 18px;
  254. padding: 14px 18px;
  255. background: #fafafa;
  256. border-radius: 8px;
  257. border: 1.5px solid #ebebeb;
  258. }
  259. .preview-header {
  260. font-size: 11px;
  261. color: #aaa;
  262. font-weight: 600;
  263. text-transform: uppercase;
  264. letter-spacing: 0.08em;
  265. margin-bottom: 10px;
  266. }
  267. .preview-tags {
  268. display: flex;
  269. flex-wrap: wrap;
  270. gap: 6px;
  271. min-height: 34px;
  272. align-items: center;
  273. margin-bottom: 10px;
  274. }
  275. .preview-tag {
  276. background: #1a1a1a;
  277. color: #fff;
  278. padding: 4px 11px;
  279. border-radius: 7px;
  280. font-size: 13px;
  281. }
  282. .preview-sep {
  283. color: #d0d0d0;
  284. font-size: 14px;
  285. font-weight: 300;
  286. }
  287. .preview-empty {
  288. color: #ccc;
  289. font-size: 13px;
  290. }
  291. .preview-path {
  292. font-size: 12px;
  293. color: #bbb;
  294. margin-bottom: 12px;
  295. min-height: 18px;
  296. font-family: monospace;
  297. letter-spacing: 0.01em;
  298. }
  299. .btn-row {
  300. display: flex;
  301. gap: 8px;
  302. flex-wrap: wrap;
  303. }
  304. .btn {
  305. border: 1px solid var(--line);
  306. background: #fff;
  307. color: var(--ink);
  308. border-radius: 8px;
  309. padding: 8px 12px;
  310. font: inherit;
  311. cursor: pointer;
  312. font-size: 13px;
  313. transition: all 0.15s;
  314. }
  315. .btn:hover {
  316. background: #f5f5f5;
  317. }
  318. .btn.active {
  319. color: #fff;
  320. background: var(--ink);
  321. border-color: var(--ink);
  322. }
  323. .btn-dark {
  324. background: #1a1a1a;
  325. color: #fff;
  326. border-color: #1a1a1a;
  327. }
  328. .btn-dark:hover {
  329. background: #333;
  330. border-color: #333;
  331. }
  332. /* Console logger styling */
  333. .exec-console {
  334. background: #181512;
  335. color: #dcd3c3;
  336. border: 1px solid #3d352e;
  337. border-radius: 12px;
  338. padding: 16px;
  339. font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
  340. box-shadow: var(--shadow);
  341. display: flex;
  342. flex-direction: column;
  343. height: 320px;
  344. }
  345. .exec-console-head {
  346. display: flex;
  347. justify-content: space-between;
  348. align-items: center;
  349. border-bottom: 1px solid #3d352e;
  350. padding-bottom: 8px;
  351. margin-bottom: 8px;
  352. font-size: 12px;
  353. color: #a3968d;
  354. }
  355. .exec-console-output {
  356. flex: 1;
  357. margin: 0;
  358. padding: 4px;
  359. overflow-y: auto;
  360. white-space: pre-wrap;
  361. word-break: break-all;
  362. font-size: 13px;
  363. line-height: 1.45;
  364. color: #e5d8c5;
  365. }
  366. /* Stats container styling */
  367. .stats {
  368. display: grid;
  369. grid-template-columns: repeat(4, 1fr);
  370. gap: 12px;
  371. margin-top: 16px;
  372. }
  373. .stat {
  374. background: var(--panel);
  375. border: 1px solid var(--line);
  376. border-radius: 8px;
  377. padding: 14px;
  378. box-shadow: var(--shadow);
  379. min-height: 86px;
  380. }
  381. .stat strong {
  382. display: block;
  383. font-size: 26px;
  384. line-height: 1.1;
  385. }
  386. .stat span {
  387. color: var(--muted);
  388. font-size: 13px;
  389. }
  390. /* Filters and Grid */
  391. .toolbar {
  392. display: flex;
  393. gap: 10px;
  394. align-items: center;
  395. justify-content: space-between;
  396. margin-top: 20px;
  397. margin-bottom: 18px;
  398. flex-wrap: wrap;
  399. }
  400. .filters {
  401. display: flex;
  402. gap: 8px;
  403. flex-wrap: wrap;
  404. }
  405. select {
  406. border: 1px solid var(--line);
  407. background: #fff;
  408. color: var(--ink);
  409. border-radius: 8px;
  410. padding: 9px 12px;
  411. font: inherit;
  412. min-width: 150px;
  413. }
  414. .grid {
  415. display: grid;
  416. grid-template-columns: repeat(2, minmax(0, 1fr));
  417. gap: 16px;
  418. }
  419. @media (max-width: 768px) {
  420. .grid {
  421. grid-template-columns: 1fr;
  422. }
  423. }
  424. /* Results cards */
  425. .result {
  426. min-height: 480px;
  427. background: var(--panel);
  428. border: 1px solid var(--line);
  429. border-radius: 8px;
  430. overflow: hidden;
  431. box-shadow: var(--shadow);
  432. display: flex;
  433. flex-direction: column;
  434. position: relative;
  435. transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.25s ease;
  436. }
  437. .result:hover {
  438. transform: translateY(-2px);
  439. box-shadow: 0 22px 50px rgba(41, 35, 28, .12);
  440. }
  441. .result.discard {
  442. border-color: rgba(178, 75, 99, 0.15);
  443. }
  444. .discard-overlay {
  445. position: absolute;
  446. top: 0;
  447. left: 0;
  448. right: 0;
  449. bottom: 0;
  450. background: rgba(251, 250, 247, 0.85);
  451. backdrop-filter: blur(5px);
  452. -webkit-backdrop-filter: blur(5px);
  453. display: flex;
  454. flex-direction: column;
  455. align-items: center;
  456. justify-content: center;
  457. padding: 20px;
  458. text-align: center;
  459. z-index: 5;
  460. opacity: 1;
  461. transition: opacity 0.25s ease;
  462. pointer-events: none;
  463. }
  464. .result.discard:hover .discard-overlay {
  465. opacity: 0;
  466. }
  467. .discard-badge {
  468. background: var(--soft-rose);
  469. color: var(--rose);
  470. border: 1px solid rgba(178, 75, 99, 0.25);
  471. padding: 6px 16px;
  472. border-radius: 999px;
  473. font-size: 13px;
  474. font-weight: 700;
  475. text-transform: uppercase;
  476. letter-spacing: 1px;
  477. margin-bottom: 12px;
  478. box-shadow: 0 2px 8px rgba(178, 75, 99, 0.1);
  479. }
  480. .discard-reason {
  481. color: var(--muted);
  482. font-size: 13px;
  483. line-height: 1.6;
  484. max-width: 90%;
  485. margin: 0 auto;
  486. display: -webkit-box;
  487. -webkit-line-clamp: 6;
  488. -webkit-box-orient: vertical;
  489. overflow: hidden;
  490. font-weight: 500;
  491. }
  492. .thumbs {
  493. display: grid;
  494. grid-template-columns: repeat(3, 1fr);
  495. gap: 2px;
  496. height: 140px;
  497. background: #eee7dc;
  498. overflow: hidden;
  499. }
  500. .thumbs img {
  501. width: 100%;
  502. height: 100%;
  503. object-fit: cover;
  504. display: block;
  505. background: #eee7dc;
  506. }
  507. .thumbs img:first-child:nth-last-child(1) {
  508. grid-column: 1 / -1;
  509. }
  510. .body {
  511. padding: 15px;
  512. flex: 1;
  513. display: flex;
  514. flex-direction: column;
  515. }
  516. .meta {
  517. display: flex;
  518. justify-content: space-between;
  519. gap: 10px;
  520. color: var(--muted);
  521. font-size: 12px;
  522. margin-bottom: 8px;
  523. }
  524. .platform {
  525. color: #fff;
  526. border-radius: 999px;
  527. padding: 2px 8px;
  528. font-weight: 700;
  529. white-space: nowrap;
  530. }
  531. .p-xhs { background: var(--rose); }
  532. .p-gzh { background: var(--mint); }
  533. .p-zhihu { background: var(--cyan); }
  534. .p-x { background: var(--cyan); }
  535. .p-bili { background: #e06d93; }
  536. .p-douyin { background: #24211d; }
  537. .p-sph { background: #07c160; }
  538. .p-youtube { background: #c4302b; }
  539. .p-github { background: #24292e; }
  540. .p-toutiao { background: #f04142; }
  541. .p-weibo { background: #e6162d; }
  542. h2 {
  543. margin: 0 0 8px;
  544. font-size: 17px;
  545. line-height: 1.3;
  546. letter-spacing: 0;
  547. }
  548. .excerpt {
  549. color: var(--muted);
  550. font-size: 13px;
  551. display: -webkit-box;
  552. -webkit-line-clamp: 4;
  553. -webkit-box-orient: vertical;
  554. overflow: hidden;
  555. margin-bottom: 10px;
  556. }
  557. .tags {
  558. display: flex;
  559. flex-wrap: wrap;
  560. gap: 6px;
  561. margin: 10px 0;
  562. }
  563. .tag {
  564. background: #f2eee7;
  565. border-radius: 999px;
  566. padding: 2px 8px;
  567. font-size: 11.5px;
  568. color: #514a42;
  569. }
  570. .scorebar {
  571. margin-top: auto;
  572. }
  573. .overall {
  574. display: flex;
  575. align-items: end;
  576. justify-content: space-between;
  577. border-top: 1px solid var(--line);
  578. padding-top: 10px;
  579. }
  580. .score {
  581. font-size: 32px;
  582. line-height: .9;
  583. font-weight: 800;
  584. }
  585. .decision {
  586. color: var(--mint);
  587. font-weight: 800;
  588. }
  589. .decision.discard {
  590. color: var(--amber);
  591. }
  592. .group-snapshot {
  593. display: grid;
  594. grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
  595. gap: 5px;
  596. margin-top: 8px;
  597. }
  598. .group-pill {
  599. border: 1px solid var(--line);
  600. border-radius: 8px;
  601. padding: 5px 6px;
  602. background: #fff;
  603. min-width: 0;
  604. }
  605. .group-pill span {
  606. display: block;
  607. color: var(--muted);
  608. font-size: 11px;
  609. white-space: nowrap;
  610. overflow: hidden;
  611. text-overflow: ellipsis;
  612. }
  613. .group-pill strong {
  614. display: block;
  615. font-size: 14px;
  616. line-height: 1.1;
  617. }
  618. .actions {
  619. display: flex;
  620. gap: 8px;
  621. margin-top: 12px;
  622. }
  623. .actions a,
  624. .actions button {
  625. flex: 1;
  626. text-align: center;
  627. text-decoration: none;
  628. color: var(--ink);
  629. background: #fff;
  630. border: 1px solid var(--line);
  631. border-radius: 8px;
  632. padding: 8px 10px;
  633. font-size: 13px;
  634. }
  635. /* Dialog styling */
  636. dialog {
  637. width: min(980px, calc(100vw - 28px));
  638. max-height: calc(100vh - 32px);
  639. border: 1px solid var(--line);
  640. border-radius: 8px;
  641. padding: 0;
  642. box-shadow: 0 28px 90px rgba(0, 0, 0, .25);
  643. }
  644. dialog::backdrop {
  645. background: rgba(38, 33, 27, .42);
  646. }
  647. dialog.fullscreen {
  648. width: 100vw !important;
  649. max-width: 100vw !important;
  650. height: 100vh !important;
  651. max-height: 100vh !important;
  652. border: none !important;
  653. border-radius: 0 !important;
  654. margin: 0 !important;
  655. top: 0 !important;
  656. left: 0 !important;
  657. }
  658. dialog.fullscreen #modalContentProcedure {
  659. height: calc(100vh - 120px) !important;
  660. max-height: none !important;
  661. }
  662. dialog.fullscreen #modalContentDetail {
  663. height: calc(100vh - 120px) !important;
  664. max-height: none !important;
  665. overflow-y: auto !important;
  666. }
  667. .modal-head {
  668. position: sticky;
  669. top: 0;
  670. background: #fff;
  671. border-bottom: 1px solid var(--line);
  672. padding: 16px;
  673. z-index: 2;
  674. display: flex;
  675. justify-content: space-between;
  676. gap: 14px;
  677. align-items: start;
  678. }
  679. .modal-head h3 {
  680. margin: 0;
  681. font-size: 20px;
  682. line-height: 1.25;
  683. }
  684. .modal-content {
  685. display: grid;
  686. grid-template-columns: 1.1fr .9fr;
  687. gap: 18px;
  688. padding: 16px;
  689. }
  690. .modal-content > section,
  691. .modal-content > aside {
  692. min-width: 0;
  693. }
  694. .section-title {
  695. margin: 18px 0 8px;
  696. font-weight: 800;
  697. }
  698. .raw {
  699. white-space: pre-wrap;
  700. background: #faf7f1;
  701. border: 1px solid var(--line);
  702. border-radius: 8px;
  703. padding: 12px;
  704. max-height: 330px;
  705. overflow: auto;
  706. color: #3d3831;
  707. font-size: 13px;
  708. }
  709. .images {
  710. display: grid;
  711. grid-template-columns: repeat(2, minmax(0, 1fr));
  712. gap: 8px;
  713. }
  714. .images img {
  715. width: 100%;
  716. max-height: 260px;
  717. object-fit: contain;
  718. border: 1px solid var(--line);
  719. border-radius: 8px;
  720. background: #f1ece4;
  721. }
  722. .scores {
  723. display: grid;
  724. gap: 12px;
  725. }
  726. .score-group {
  727. border: 1px solid var(--line);
  728. border-radius: 8px;
  729. background: #fff;
  730. overflow: hidden;
  731. }
  732. .score-group-head {
  733. display: flex;
  734. justify-content: space-between;
  735. gap: 10px;
  736. align-items: center;
  737. padding: 10px 11px;
  738. background: #faf7f1;
  739. border-bottom: 1px solid var(--line);
  740. font-size: 13px;
  741. font-weight: 800;
  742. }
  743. .score-group-head small {
  744. color: var(--muted);
  745. font-weight: 700;
  746. white-space: nowrap;
  747. }
  748. .score-group-body {
  749. display: grid;
  750. gap: 8px;
  751. padding: 10px;
  752. }
  753. .score-row {
  754. display: grid;
  755. grid-template-columns: 128px 1fr 34px;
  756. gap: 10px;
  757. align-items: center;
  758. font-size: 13px;
  759. }
  760. .score-row.missing {
  761. color: #a39b91;
  762. }
  763. .score-row.missing .meter span {
  764. display: none;
  765. }
  766. .meter {
  767. height: 9px;
  768. border-radius: 999px;
  769. background: #eee7dc;
  770. overflow: hidden;
  771. }
  772. .meter span {
  773. display: block;
  774. height: 100%;
  775. width: calc(var(--v) * 20%);
  776. background: var(--rose);
  777. }
  778. .rubric-note {
  779. background: var(--soft-cyan);
  780. border-left: 4px solid var(--cyan);
  781. padding: 10px 12px;
  782. color: #254c5d;
  783. border-radius: 4px;
  784. font-size: 13px;
  785. }
  786. .modal-tabs {
  787. display: flex;
  788. gap: 4px;
  789. padding: 0 16px;
  790. border-bottom: 1px solid var(--line);
  791. background: #faf7f1;
  792. }
  793. .modal-tab {
  794. background: transparent;
  795. border: none;
  796. border-bottom: 3px solid transparent;
  797. border-radius: 0;
  798. padding: 10px 16px;
  799. font-size: 14px;
  800. font-weight: 600;
  801. color: var(--muted);
  802. cursor: pointer;
  803. transition: all 0.2s ease;
  804. }
  805. .modal-tab:hover {
  806. color: var(--ink);
  807. background: rgba(0, 0, 0, 0.02);
  808. }
  809. .modal-tab.active {
  810. color: var(--mint);
  811. border-bottom-color: var(--mint);
  812. }
  813. /* Rating card styling */
  814. .sc-card {
  815. background: #fff;
  816. border: 1px solid var(--line);
  817. border-radius: 12px;
  818. padding: 16px 20px;
  819. margin-bottom: 16px;
  820. box-shadow: 0 4px 15px rgba(0,0,0,0.02);
  821. }
  822. .sc-card-head {
  823. display: flex;
  824. justify-content: space-between;
  825. align-items: center;
  826. margin-bottom: 14px;
  827. border-bottom: 1px solid #f3f0ea;
  828. padding-bottom: 10px;
  829. }
  830. .sc-card-head .title {
  831. font-size: 15px;
  832. font-weight: 700;
  833. display: flex;
  834. align-items: center;
  835. gap: 8px;
  836. }
  837. .sc-card-head .badge {
  838. background: #eef2ff;
  839. color: #2563eb;
  840. font-size: 11px;
  841. padding: 2px 6px;
  842. border-radius: 4px;
  843. font-weight: 700;
  844. }
  845. .sc-card-head .avg-score {
  846. font-size: 13.5px;
  847. color: var(--muted);
  848. font-weight: 600;
  849. }
  850. .sc-card-head .avg-score strong {
  851. font-size: 24px;
  852. color: #2563eb;
  853. font-weight: 800;
  854. margin-left: 4px;
  855. }
  856. .sc-sub-header {
  857. font-size: 12px;
  858. color: var(--muted);
  859. font-weight: 700;
  860. margin: 14px 0 8px;
  861. border-bottom: 1px dashed #f0ebd8;
  862. padding-bottom: 4px;
  863. text-transform: uppercase;
  864. letter-spacing: 0.5px;
  865. }
  866. .sc-row {
  867. display: flex;
  868. justify-content: space-between;
  869. align-items: center;
  870. padding: 6px 0;
  871. font-size: 13px;
  872. gap: 10px;
  873. }
  874. .sc-row .label {
  875. color: var(--ink);
  876. font-weight: 500;
  877. flex: 1;
  878. min-width: 100px;
  879. word-break: break-all;
  880. }
  881. .sc-row .bar-wrap {
  882. display: flex;
  883. align-items: center;
  884. gap: 10px;
  885. width: 160px;
  886. flex-shrink: 0;
  887. }
  888. .sc-row .bar {
  889. height: 6px;
  890. background: #eee7dc;
  891. border-radius: 999px;
  892. flex: 1;
  893. overflow: hidden;
  894. }
  895. .sc-row .bar-fill {
  896. height: 100%;
  897. background: #2563eb;
  898. border-radius: 999px;
  899. width: calc(var(--v) * 10%);
  900. }
  901. .sc-row .value {
  902. font-weight: 700;
  903. font-size: 13px;
  904. width: 20px;
  905. text-align: right;
  906. }
  907. .sc-row .info-icon {
  908. cursor: pointer;
  909. color: #9ca3af;
  910. transition: color 0.15s ease;
  911. font-size: 13px;
  912. user-select: none;
  913. }
  914. .sc-row .info-icon:hover {
  915. color: #3b82f6;
  916. }
  917. </style>
  918. </head>
  919. <body>
  920. <header>
  921. <div class="wrap">
  922. <div class="eyebrow">Content Search · runs_new/ 实时 · 维度构建器</div>
  923. <h1>新 Query 搜索与评估</h1>
  924. <p class="lede" id="lede">正在扫描 runs_new/ 目录...</p>
  925. </div>
  926. </header>
  927. <main class="app-container">
  928. <!-- Sidebar: previously searched queries -->
  929. <aside class="sidebar">
  930. <h3>
  931. <span>已检索 Query 列表</span>
  932. <button class="btn" onclick="loadData(true)" style="padding: 3px 8px; font-size: 11px;">刷新</button>
  933. </h3>
  934. <div class="sidebar-list" id="querySidebarList">
  935. <!-- Dynamic sidebar items -->
  936. </div>
  937. </aside>
  938. <!-- Main Workspace -->
  939. <div style="display: flex; flex-direction: column; gap: 24px; min-width: 0;">
  940. <!-- Query Builder Panel -->
  941. <section class="builder-card">
  942. <h3>Query 词组织器</h3>
  943. <div class="dimensions" id="dimensions"></div>
  944. <div class="preview-section">
  945. <div class="preview-header">Query 预览</div>
  946. <div class="preview-tags" id="previewTags">
  947. <span class="preview-empty">(请选择维度标签)</span>
  948. </div>
  949. <div class="preview-path" id="previewPath"></div>
  950. <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px;">
  951. <div style="display: flex; flex-direction: column; gap: 8px; align-items: flex-start;">
  952. <div style="display: flex; align-items: center; gap: 16px;">
  953. <span style="font-size: 13.5px; font-weight: 700; color: var(--muted);">检索渠道:</span>
  954. <div style="display: flex; gap: 10px; flex-wrap: wrap;" id="platformCheckboxes">
  955. <!-- 小红书 -->
  956. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s;">
  957. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  958. <input type="checkbox" name="platform" value="xhs" checked onchange="togglePlatformInput(this)"> 小红书
  959. </label>
  960. <input type="number" name="platform_count" data-platform="xhs" min="1" max="100" value="20" style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink);">
  961. </div>
  962. <!-- 知乎 -->
  963. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s;">
  964. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  965. <input type="checkbox" name="platform" value="zhihu" checked onchange="togglePlatformInput(this)"> 知乎
  966. </label>
  967. <input type="number" name="platform_count" data-platform="zhihu" min="1" max="100" value="20" style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink);">
  968. </div>
  969. <!-- 公众号 -->
  970. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
  971. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  972. <input type="checkbox" name="platform" value="gzh" onchange="togglePlatformInput(this)"> 公众号
  973. </label>
  974. <input type="number" name="platform_count" data-platform="gzh" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
  975. </div>
  976. <!-- 抖音 -->
  977. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
  978. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  979. <input type="checkbox" name="platform" value="douyin" onchange="togglePlatformInput(this)"> 抖音
  980. </label>
  981. <input type="number" name="platform_count" data-platform="douyin" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
  982. </div>
  983. <!-- 视频号 -->
  984. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
  985. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  986. <input type="checkbox" name="platform" value="sph" onchange="togglePlatformInput(this)"> 视频号
  987. </label>
  988. <input type="number" name="platform_count" data-platform="sph" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
  989. </div>
  990. <!-- YouTube -->
  991. <div style="display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; transition: opacity 0.15s; opacity: 0.6;">
  992. <label style="font-size: 13.5px; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font-weight: 500;">
  993. <input type="checkbox" name="platform" value="youtube" onchange="togglePlatformInput(this)"> YouTube
  994. </label>
  995. <input type="number" name="platform_count" data-platform="youtube" min="1" max="100" value="20" disabled style="width: 38px; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 3px; font-size: 11.5px; text-align: center; font-weight: bold; outline: none; color: var(--ink); opacity: 0.4;">
  996. </div>
  997. </div>
  998. </div>
  999. <div id="channelCountsRow" style="font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 8px; flex-wrap: wrap;"></div>
  1000. </div>
  1001. <div class="btn-row">
  1002. <button class="btn btn-dark" id="searchBtn" onclick="runSearchAndEvaluate()" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: 700; padding: 6px 18px;">⚡ 搜索并评估</button>
  1003. <button class="btn" onclick="clearAll()">清空</button>
  1004. </div>
  1005. </div>
  1006. </div>
  1007. </section>
  1008. <!-- Subprocess log card -->
  1009. <section class="exec-console" id="execConsoleCard" style="display: none;">
  1010. <div class="exec-console-head">
  1011. <span id="consoleTitle">🚀 搜索并评估任务控制台 - 准备就绪</span>
  1012. <span id="consoleStatus" style="font-weight: bold; color: var(--amber);">idle</span>
  1013. </div>
  1014. <pre class="exec-console-output" id="consoleOutput"></pre>
  1015. </section>
  1016. <!-- Grid Cards Area -->
  1017. <div id="resultsArea" style="display: none;">
  1018. <div class="stats" id="stats"></div>
  1019. <div class="toolbar">
  1020. <div class="filters" id="platformFilterWrap"></div>
  1021. <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
  1022. <div class="threshold-control">
  1023. <span style="color: var(--muted); font-size: 12px; font-weight: 700;">相关性过滤阈值:</span>
  1024. <input type="number" id="relThreshold" min="0" max="10" step="0.5" value="4.0"
  1025. oninput="renderGrid(); renderHead();"
  1026. style="width: 55px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 6px; font-weight: 700; text-align: center; color: #2563eb; outline: none;">
  1027. </div>
  1028. <button id="reevalBtn" class="btn" onclick="reevalCurrentQuery()" style="background: #faf7f1; color: var(--amber); border-color: rgba(184, 121, 24, 0.3);">♻️ 重新评估当前结果</button>
  1029. <button id="editSpecBtn" class="btn" onclick="openSpecEditor()" style="background: #f0f7f6; color: var(--cyan); border-color: rgba(42, 111, 143, 0.3);">📝 编辑 Spec 提示词</button>
  1030. <div style="display: flex; align-items: center; gap: 4px; background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 4px 10px; font-size: 13px; font-weight: 600; box-shadow: var(--shadow); height: 38px;">
  1031. <span style="color: var(--muted); font-size: 12px; font-weight: 700;">提取并发:</span>
  1032. <input type="number" id="batchConcurrency" min="1" max="16" value="4"
  1033. style="width: 40px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 4px; font-weight: 700; text-align: center; color: var(--ink); outline: none;">
  1034. </div>
  1035. <button id="batchProcBtn" class="btn" onclick="batchExtractProcedures()" style="background: #f0fdf4; color: var(--mint); border-color: rgba(31, 138, 112, 0.3); font-weight: 600; height: 38px;">⚡ 一键提取工序</button>
  1036. <select id="sort" onchange="renderGrid()">
  1037. <option value="score">按综合分排序</option>
  1038. <option value="date">按发布时间排序</option>
  1039. <option value="platform">按平台排序</option>
  1040. </select>
  1041. </div>
  1042. </div>
  1043. <div class="grid" id="grid"></div>
  1044. </div>
  1045. </div>
  1046. </main>
  1047. <!-- Details Dialog Modal -->
  1048. <dialog id="detailDialog">
  1049. <div class="modal-head">
  1050. <div>
  1051. <div id="modalMeta" class="meta"></div>
  1052. <h3 id="modalTitle"></h3>
  1053. </div>
  1054. <div style="display: flex; gap: 8px; align-items: center;">
  1055. <button id="toggleFullscreenBtn" class="btn" onclick="toggleModalFullscreen()" style="padding: 5px 12px; font-weight: 600; font-size: 12.5px;">📺 全屏</button>
  1056. <button class="btn" onclick="detailDialog.close()">关闭</button>
  1057. </div>
  1058. </div>
  1059. <div class="modal-tabs" id="modalTabs" style="display: none;">
  1060. <button class="modal-tab active" onclick="switchModalTab('detail')" id="tabDetailBtn">帖子详情</button>
  1061. <button class="modal-tab" onclick="switchModalTab('procedure')" id="tabProcedureBtn">对应工序</button>
  1062. </div>
  1063. <div class="modal-content" id="modalContentDetail">
  1064. <section>
  1065. <div class="rubric-note" id="modalReason"></div>
  1066. <div class="section-title">抓取文本节选</div>
  1067. <div class="raw" id="modalText"></div>
  1068. <div class="section-title">图片预览</div>
  1069. <div class="images" id="modalImages"></div>
  1070. </section>
  1071. <aside>
  1072. <div class="section-title" style="display: flex; justify-content: space-between; align-items: center;">
  1073. <span>评分详情</span>
  1074. <span id="modalOverallScore" style="font-size: 13px; 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;">
  1075. 综合评分 <strong style="font-size: 19px; color: #2563eb; font-weight: 900; line-height: 1;" id="modalOverallScoreVal">—</strong>
  1076. </span>
  1077. </div>
  1078. <div class="scores" id="modalScores"></div>
  1079. <div class="section-title">类型 / 命中 query</div>
  1080. <div class="tags" id="modalTags"></div>
  1081. </aside>
  1082. </div>
  1083. <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;">
  1084. <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;">
  1085. <div style="font-size: 14px; font-weight: bold; color: var(--ink);" id="procStatusText">工序状态: 检测中...</div>
  1086. <div style="display: flex; gap: 8px;" id="procActionBtns"></div>
  1087. </div>
  1088. <div style="flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column;">
  1089. <div id="procSetupPanel" style="display: none; flex-direction: column; align-items: center; justify-content: center; text-align: center; height: 100%; padding: 40px 20px;">
  1090. <div style="font-size: 36px; margin-bottom: 16px;">✨</div>
  1091. <h4 style="margin: 0 0 10px; font-size: 18px;">提取本帖工序</h4>
  1092. <p style="color: var(--muted); font-size: 14px; max-width: 500px; margin: 0 0 24px;">该帖子目前尚未生成对应的结构化工序。请在下方选择提取引擎和模型,点击开始提取。</p>
  1093. <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);">
  1094. <div>
  1095. <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">提取引擎 (Engine)</label>
  1096. <select id="procEngineSelect" style="min-width: 180px; padding: 6px 10px;" onchange="onProcEngineChange()">
  1097. <option value="cyber_runner">Cyber Runner (自研/OpenRouter)</option>
  1098. <option value="claude_sdk">Claude SDK (OAuth)</option>
  1099. </select>
  1100. </div>
  1101. <div>
  1102. <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">AI 模型 (Model)</label>
  1103. <select id="procModelSelect" style="min-width: 240px; padding: 6px 10px;"></select>
  1104. </div>
  1105. </div>
  1106. <button id="startProcBtn" class="btn btn-dark" onclick="startProcedureExtraction()" style="background: var(--mint); border-color: var(--mint); padding: 10px 24px; font-size: 15px; font-weight: bold;">开始提取工序</button>
  1107. </div>
  1108. <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;">
  1109. <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;">
  1110. <span>TERMINAL CONSOLE</span>
  1111. <span id="procConsoleStatus">idle</span>
  1112. </div>
  1113. <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>
  1114. </div>
  1115. <iframe id="procedureIframe" style="display: none; width: 100%; height: 100%; border: 1px solid var(--line); border-radius: 8px; background: #fff;" referrerpolicy="no-referrer"></iframe>
  1116. </div>
  1117. </div>
  1118. </dialog>
  1119. <!-- Spec Prompt Editor Dialog -->
  1120. <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);">
  1121. <div style="display: flex; justify-content: space-between; align-items: center; background: #faf7f1; border-bottom: 1px solid var(--line); padding: 16px 20px;">
  1122. <h3 style="margin: 0; font-size: 18px; color: var(--ink); font-weight: 800;">📝 编辑 Spec 提示词规范</h3>
  1123. <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="padding: 5px 14px; font-weight: 600; font-size: 12px;">关闭</button>
  1124. </div>
  1125. <div style="padding: 20px;">
  1126. <div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
  1127. <span style="font-size: 13.5px; font-weight: 700; color: var(--ink);">选择提示词文件:</span>
  1128. <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;">
  1129. </select>
  1130. <span id="specLoadStatus" style="font-size: 13px; font-weight: 600;"></span>
  1131. </div>
  1132. <div style="position: relative; margin-bottom: 20px;">
  1133. <textarea id="specContentTextarea" style="width: 100%; height: 500px; font-family: ui-monospace, Menlo, Monaco, Consolas, 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);"></textarea>
  1134. </div>
  1135. <div style="display: flex; justify-content: flex-end; gap: 12px; align-items: center; border-top: 1px solid var(--line); padding-top: 16px;">
  1136. <span id="specSaveStatus" style="font-size: 13.5px; font-weight: 700; margin-right: auto;"></span>
  1137. <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="background: #f3f4f6;">取消</button>
  1138. <button onclick="saveSpecFileContent()" class="btn" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: bold;">💾 保存修改</button>
  1139. </div>
  1140. </div>
  1141. </dialog>
  1142. <script>
  1143. const DIMS = [
  1144. {
  1145. "id": "tool_type",
  1146. "label": "工具类型",
  1147. "type": "flat",
  1148. "items": ["AI", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"]
  1149. },
  1150. {
  1151. "id": "substance",
  1152. "label": "实质",
  1153. "type": "flat",
  1154. "items": ["人像", "信息"]
  1155. },
  1156. {
  1157. "id": "form",
  1158. "label": "形式",
  1159. "type": "flat",
  1160. "items": ["真实感", "写实风格", "实景拍摄", "版面设计", "版面解构"]
  1161. },
  1162. {
  1163. "id": "modality",
  1164. "label": "模态",
  1165. "type": "flat",
  1166. "items": ["图片", "视频"]
  1167. },
  1168. {
  1169. "id": "action",
  1170. "label": "动作",
  1171. "type": "hierarchical",
  1172. "data": {
  1173. "获取": {
  1174. "搜索": ["检索", "下载"],
  1175. "查询": ["调取"],
  1176. "录入": ["上传", "拍摄", "录音", "键入"],
  1177. "引用": ["选取"]
  1178. },
  1179. "提取": {
  1180. "物理提取": ["裁切", "抠取", "抽帧"],
  1181. "化学提取": ["识别", "反推", "解构"]
  1182. },
  1183. "生成": {
  1184. "元素生成": ["元素生成"],
  1185. "关系生成": ["数组生成", "结构生成"]
  1186. },
  1187. "修改": {
  1188. "增": ["添加", "叠加"],
  1189. "删": ["抹除", "剪除"],
  1190. "变": ["重述", "风格化", "转换", "替换", "调整", "增强"]
  1191. }
  1192. }
  1193. },
  1194. {
  1195. "id": "type",
  1196. "label": "类型",
  1197. "type": "hierarchical",
  1198. "data": {
  1199. "程序控制类型": {
  1200. "指令": ["提示词", "负向提示词", "描述"],
  1201. "参数": ["生成参数", "规格参数", "模型权重"],
  1202. "评估": ["评分", "评语"],
  1203. "流程": ["工作流", "批处理"]
  1204. },
  1205. "数据复用类型": {
  1206. "原子": ["数字人", "版式"],
  1207. "序列": ["模板"]
  1208. },
  1209. "内容类型": {
  1210. "素材/化学变化": ["参考图", "参考视频", "参考音频", "对标内容", "分镜图", "转场", "蒙版", "控制图", "运动轨迹", "滤镜", "构图布局"],
  1211. "素材/物理变化": ["截图", "视频片段", "转场片段", "关键帧", "音效", "特效"],
  1212. "半成品/序列": ["大纲", "脚本", "分镜脚本", "剪辑脚本", "配音文案"],
  1213. "半成品/原子": ["底图", "样图", "分镜视频"],
  1214. "半成品/组合": ["图层组合", "拼图"],
  1215. "准成品": ["歌词", "配音", "BGM", "字幕", "标题", "正文"],
  1216. "成品": ["成品图", "视频成品", "合成图"]
  1217. },
  1218. "知识类型": {
  1219. "知识库": ["知识库"]
  1220. }
  1221. }
  1222. },
  1223. {
  1224. "id": "suffix",
  1225. "label": "后缀",
  1226. "type": "flat",
  1227. "default": null,
  1228. "items": ["怎么做"]
  1229. }
  1230. ];
  1231. const builderState = {};
  1232. DIMS.forEach(d => { builderState[d.id] = { l0: d.default !== undefined ? d.default : null, l1: null, l2: null }; });
  1233. let DATA = { queries: [], actions: [], types: [], matrix: [] };
  1234. let st = { qi: -1, fi: 0, channel: "all", sort: "score" };
  1235. let VIEW = [];
  1236. let currentProcTask = null;
  1237. let procPollInterval = null;
  1238. let isLogViewActive = false;
  1239. let searchPollInterval = null;
  1240. let reevalPollIntervals = {};
  1241. // Chip Rendering functions
  1242. function makeRow(indent) {
  1243. const el = document.createElement('div');
  1244. el.className = 'level-row' + (indent ? ' indent' : '');
  1245. return el;
  1246. }
  1247. function makeLabel(text, small) {
  1248. const el = document.createElement('span');
  1249. el.className = small ? 'sub-label' : 'dim-label';
  1250. el.textContent = text;
  1251. return el;
  1252. }
  1253. function makeChipsWrap() {
  1254. const el = document.createElement('div');
  1255. el.className = 'chips-wrap';
  1256. return el;
  1257. }
  1258. function makeChip(label, cls, onClick) {
  1259. const btn = document.createElement('button');
  1260. btn.className = 'chip ' + cls;
  1261. btn.textContent = label;
  1262. btn.addEventListener('click', onClick);
  1263. return btn;
  1264. }
  1265. function renderDim(dimId) {
  1266. const dim = DIMS.find(d => d.id === dimId);
  1267. const grp = document.querySelector('.dim-group[data-id="' + dimId + '"]');
  1268. grp.innerHTML = '';
  1269. const sel = builderState[dimId];
  1270. if (dim.type === 'flat') {
  1271. const row = makeRow(false);
  1272. row.appendChild(makeLabel(dim.label, false));
  1273. if (!dim.items || !dim.items.length) {
  1274. const ph = document.createElement('span');
  1275. ph.className = 'placeholder-text';
  1276. ph.textContent = '(待填写)';
  1277. row.appendChild(ph);
  1278. } else {
  1279. const chips = makeChipsWrap();
  1280. chips.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
  1281. builderState[dimId].l0 = null; renderDim(dimId); updateQueryPreview();
  1282. }));
  1283. dim.items.forEach(item => {
  1284. chips.appendChild(makeChip(item, sel.l0 === item ? 'selected' : '', () => {
  1285. builderState[dimId].l0 = item; renderDim(dimId); updateQueryPreview();
  1286. }));
  1287. });
  1288. row.appendChild(chips);
  1289. }
  1290. grp.appendChild(row);
  1291. return;
  1292. }
  1293. if (dim.type === 'hierarchical') {
  1294. const row0 = makeRow(false);
  1295. row0.appendChild(makeLabel(dim.label, false));
  1296. const chips0 = makeChipsWrap();
  1297. chips0.appendChild(makeChip('无', sel.l0 == null ? 'none-active' : '', () => {
  1298. builderState[dimId] = { l0: null, l1: null, l2: null }; renderDim(dimId); updateQueryPreview();
  1299. }));
  1300. Object.keys(dim.data).forEach(key => {
  1301. chips0.appendChild(makeChip(key, sel.l0 === key ? 'selected' : '', () => {
  1302. builderState[dimId] = { l0: key, l1: null, l2: null }; renderDim(dimId); updateQueryPreview();
  1303. }));
  1304. });
  1305. row0.appendChild(chips0);
  1306. grp.appendChild(row0);
  1307. if (sel.l0 && dim.data[sel.l0]) {
  1308. const L1keys = Object.keys(dim.data[sel.l0]);
  1309. const row1 = makeRow(true);
  1310. row1.appendChild(makeLabel(sel.l0, true));
  1311. const chips1 = makeChipsWrap();
  1312. chips1.appendChild(makeChip('无', sel.l1 == null ? 'none-active' : '', () => {
  1313. builderState[dimId].l1 = null; builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
  1314. }));
  1315. L1keys.forEach(key => {
  1316. chips1.appendChild(makeChip(key, sel.l1 === key ? 'selected' : '', () => {
  1317. builderState[dimId].l1 = key; builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
  1318. }));
  1319. });
  1320. row1.appendChild(chips1);
  1321. grp.appendChild(row1);
  1322. }
  1323. if (sel.l0 && sel.l1 && dim.data[sel.l0] && dim.data[sel.l0][sel.l1]) {
  1324. const L2items = dim.data[sel.l0][sel.l1];
  1325. const row2 = makeRow(true);
  1326. row2.appendChild(makeLabel(sel.l1, true));
  1327. const chips2 = makeChipsWrap();
  1328. chips2.appendChild(makeChip('无', sel.l2 == null ? 'none-active' : '', () => {
  1329. builderState[dimId].l2 = null; renderDim(dimId); updateQueryPreview();
  1330. }));
  1331. L2items.forEach(item => {
  1332. chips2.appendChild(makeChip(item, sel.l2 === item ? 'selected' : '', () => {
  1333. builderState[dimId].l2 = item; renderDim(dimId); updateQueryPreview();
  1334. }));
  1335. });
  1336. row2.appendChild(chips2);
  1337. grp.appendChild(row2);
  1338. }
  1339. }
  1340. }
  1341. function getSelections() {
  1342. return DIMS.map(dim => {
  1343. const sel = builderState[dim.id];
  1344. if (dim.type === 'flat') {
  1345. return sel.l0 ? { dim: dim.label, path: [sel.l0], val: sel.l0 } : null;
  1346. }
  1347. if (dim.type === 'hierarchical') {
  1348. const path = [sel.l0, sel.l1, sel.l2].filter(Boolean);
  1349. if (!path.length) return null;
  1350. return { dim: dim.label, path, val: path[path.length - 1] };
  1351. }
  1352. return null;
  1353. }).filter(Boolean);
  1354. }
  1355. function updateQueryPreview() {
  1356. const sels = getSelections();
  1357. const tagsEl = document.getElementById('previewTags');
  1358. const pathEl = document.getElementById('previewPath');
  1359. tagsEl.innerHTML = '';
  1360. pathEl.textContent = '';
  1361. if (!sels.length) {
  1362. tagsEl.innerHTML = '<span class="preview-empty">(请选择维度标签)</span>';
  1363. return;
  1364. }
  1365. sels.forEach((s, i) => {
  1366. if (i > 0) {
  1367. const sep = document.createElement('span');
  1368. sep.className = 'preview-sep';
  1369. sep.textContent = '·';
  1370. tagsEl.appendChild(sep);
  1371. }
  1372. const tag = document.createElement('span');
  1373. tag.className = 'preview-tag';
  1374. tag.textContent = s.val;
  1375. tagsEl.appendChild(tag);
  1376. });
  1377. pathEl.textContent = sels.map(s => '[' + s.dim + '] ' + s.path.join(' › ')).join(' ');
  1378. }
  1379. function getQueryText() {
  1380. return getSelections().map(s => s.val).join(' ');
  1381. }
  1382. function clearAll() {
  1383. DIMS.forEach(d => { builderState[d.id] = { l0: null, l1: null, l2: null }; });
  1384. DIMS.forEach(d => renderDim(d.id));
  1385. updateQueryPreview();
  1386. }
  1387. // Sidebar navigation functions
  1388. function renderSidebar() {
  1389. const list = document.getElementById("querySidebarList");
  1390. list.innerHTML = "";
  1391. if (!DATA.queries.length) {
  1392. list.innerHTML = `<div style="color: #ccc; font-size: 13px; text-align: center; padding: 24px 0;">(暂无检索记录,请在上方组织新 Query 进行搜索)</div>`;
  1393. document.getElementById("resultsArea").style.display = "none";
  1394. return;
  1395. }
  1396. DATA.queries.forEach((q, i) => {
  1397. const div = document.createElement("div");
  1398. div.className = "sidebar-item" + (st.qi === i ? " active" : "");
  1399. div.innerHTML = `
  1400. <div class="q-text">${esc(q.original_q)}</div>
  1401. <div class="meta">
  1402. <span>采纳/命中: <strong>${q.hits}</strong></span>
  1403. <span>总帖数: ${q.tot}</span>
  1404. </div>
  1405. `;
  1406. div.onclick = () => {
  1407. st.qi = i;
  1408. st.fi = 0;
  1409. st.channel = "all";
  1410. renderSidebar();
  1411. renderGrid();
  1412. renderHead();
  1413. document.getElementById("resultsArea").style.display = "block";
  1414. };
  1415. list.appendChild(div);
  1416. });
  1417. }
  1418. function togglePlatformInput(chk) {
  1419. const parent = chk.closest('div');
  1420. if (!parent) return;
  1421. const numInput = parent.querySelector('input[type="number"]');
  1422. if (!numInput) return;
  1423. if (chk.checked) {
  1424. numInput.disabled = false;
  1425. numInput.style.opacity = "1";
  1426. parent.style.opacity = "1";
  1427. } else {
  1428. numInput.disabled = true;
  1429. numInput.style.opacity = "0.4";
  1430. parent.style.opacity = "0.6";
  1431. }
  1432. }
  1433. // Search and evaluation trigger & log polling
  1434. function runSearchAndEvaluate() {
  1435. const q = getQueryText();
  1436. if (!q) {
  1437. alert("请先选择标签,组成要检索的 Query 词!");
  1438. return;
  1439. }
  1440. const checkboxes = document.querySelectorAll('input[name="platform"]:checked');
  1441. if (checkboxes.length === 0) {
  1442. alert("请至少选择一个目标检索渠道!");
  1443. return;
  1444. }
  1445. const platforms = Array.from(checkboxes).map(c => {
  1446. const parent = c.closest('div');
  1447. const numInput = parent ? parent.querySelector('input[type="number"]') : null;
  1448. const count = numInput ? parseInt(numInput.value) : 20;
  1449. return `${c.value}:${count}`;
  1450. }).join(",");
  1451. const searchBtn = document.getElementById("searchBtn");
  1452. searchBtn.disabled = true;
  1453. searchBtn.textContent = "⌛ 正在启动...";
  1454. const consoleCard = document.getElementById("execConsoleCard");
  1455. consoleCard.style.display = "flex";
  1456. const consoleOutput = document.getElementById("consoleOutput");
  1457. consoleOutput.textContent = "⏳ 正在向服务器提交搜索评估任务...\n";
  1458. const statusSpan = document.getElementById("consoleStatus");
  1459. statusSpan.textContent = "starting";
  1460. statusSpan.style.color = "var(--amber)";
  1461. document.getElementById("consoleTitle").textContent = `🚀 搜索并评估任务控制台 - [${q}]`;
  1462. fetch("/api/run_search_eval", {
  1463. method: "POST",
  1464. headers: { "Content-Type": "application/json" },
  1465. body: JSON.stringify({ query: q, platforms: platforms })
  1466. })
  1467. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  1468. .then(({ ok, d }) => {
  1469. if (!ok) {
  1470. searchBtn.disabled = false;
  1471. searchBtn.textContent = "⚡ 搜索并评估";
  1472. statusSpan.textContent = "failed";
  1473. statusSpan.style.color = "var(--rose)";
  1474. consoleOutput.textContent += `❌ 启动失败: ${d.error || "未知错误"}\n`;
  1475. return;
  1476. }
  1477. statusSpan.textContent = "running";
  1478. statusSpan.style.color = "#00ff00";
  1479. consoleOutput.textContent += `✓ 搜评流程异步启动完成,正在获取实时日志...\n`;
  1480. startSearchPolling(q);
  1481. })
  1482. .catch(err => {
  1483. searchBtn.disabled = false;
  1484. searchBtn.textContent = "⚡ 搜索并评估";
  1485. statusSpan.textContent = "failed";
  1486. statusSpan.style.color = "var(--rose)";
  1487. consoleOutput.textContent += `❌ 请求失败: ${err}\n`;
  1488. });
  1489. }
  1490. function startSearchPolling(q) {
  1491. if (searchPollInterval) clearInterval(searchPollInterval);
  1492. const statusSpan = document.getElementById("consoleStatus");
  1493. const consoleOutput = document.getElementById("consoleOutput");
  1494. const searchBtn = document.getElementById("searchBtn");
  1495. const poll = () => {
  1496. fetch(`/api/search_eval_status?q=${encodeURIComponent(q)}`)
  1497. .then(r => r.json())
  1498. .then(d => {
  1499. if (d.status === "success") {
  1500. clearInterval(searchPollInterval);
  1501. searchPollInterval = null;
  1502. statusSpan.textContent = "success";
  1503. statusSpan.style.color = "var(--mint)";
  1504. searchBtn.disabled = false;
  1505. searchBtn.textContent = "⚡ 搜索并评估";
  1506. consoleOutput.textContent += "\n🎉 搜评流程全部成功执行完毕,已保存于 runs_new 下!\n";
  1507. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  1508. loadData(true);
  1509. } else if (d.status === "failed") {
  1510. clearInterval(searchPollInterval);
  1511. searchPollInterval = null;
  1512. statusSpan.textContent = "failed";
  1513. statusSpan.style.color = "var(--rose)";
  1514. searchBtn.disabled = false;
  1515. searchBtn.textContent = "⚡ 搜索并评估";
  1516. consoleOutput.textContent += `\n❌ 搜评任务执行失败: ${d.error || "未知原因"}\n`;
  1517. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  1518. loadData(true);
  1519. }
  1520. });
  1521. fetch(`/api/search_eval_log?q=${encodeURIComponent(q)}`)
  1522. .then(r => r.json())
  1523. .then(d => {
  1524. if (d.log) {
  1525. consoleOutput.textContent = d.log;
  1526. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  1527. }
  1528. });
  1529. };
  1530. poll();
  1531. searchPollInterval = setInterval(poll, 1500);
  1532. }
  1533. // Grid rendering logic
  1534. function curForm() {
  1535. return st.qi === -1 ? null : (DATA.queries[st.qi] ? DATA.queries[st.qi].forms[st.fi] : null);
  1536. }
  1537. function isItemDiscarded(it) {
  1538. if (it.anomaly) return false;
  1539. const input = document.getElementById("relThreshold");
  1540. const userThreshold = input ? parseFloat(input.value) : 4.0;
  1541. let isDiscard = false;
  1542. const relVal = it.production_relevance !== null && it.production_relevance !== undefined ? parseFloat(it.production_relevance) : null;
  1543. if (relVal !== null && !isNaN(relVal)) {
  1544. if (relVal < userThreshold) {
  1545. isDiscard = true;
  1546. }
  1547. }
  1548. if (it.recency_hard !== null && it.recency_hard !== undefined && it.recency_hard < 2) {
  1549. isDiscard = true;
  1550. }
  1551. if (it.overall !== null && it.overall !== undefined) {
  1552. if (it.overall < 6.0) {
  1553. isDiscard = true;
  1554. }
  1555. }
  1556. return isDiscard;
  1557. }
  1558. function updateThresholdLimits() {
  1559. const input = document.getElementById("relThreshold");
  1560. if (input) {
  1561. input.max = "10";
  1562. input.value = "4.0";
  1563. input.step = "0.5";
  1564. }
  1565. }
  1566. function updateChannelCounts() {
  1567. const f = curForm();
  1568. const div = document.getElementById("channelCountsRow");
  1569. if (!div) return;
  1570. if (!f || !f.results || f.results.length === 0) {
  1571. div.innerHTML = "";
  1572. return;
  1573. }
  1574. const counts = {};
  1575. f.results.forEach(r => {
  1576. const pk = r.platformKey || "other";
  1577. counts[pk] = (counts[pk] || 0) + 1;
  1578. });
  1579. const parts = [];
  1580. const sortedKeys = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
  1581. sortedKeys.forEach(pk => {
  1582. const name = PLATC[pk] || pk;
  1583. parts.push(`
  1584. <span style="background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; display: inline-flex; align-items: center; gap: 6px; font-weight: 500; font-size: 11.5px; box-shadow: 0 1px 3px rgba(0,0,0,0.02);">
  1585. <span class="platform p-${pk}" style="width: 7px; height: 7px; border-radius: 50%; display: inline-block; padding: 0; margin-right: 0;"></span>
  1586. <span style="color: var(--muted);">${name}</span>
  1587. <strong style="color: var(--ink); font-weight: 700;">${counts[pk]}</strong>
  1588. </span>
  1589. `);
  1590. });
  1591. div.innerHTML = `<span style="font-weight: 700; color: var(--muted); margin-right: 4px; font-size: 12px;">已收录帖数:</span>` + parts.join("");
  1592. }
  1593. function renderHead() {
  1594. const f = curForm();
  1595. const div = document.getElementById("stats");
  1596. if (!f) {
  1597. div.innerHTML = "";
  1598. const cDiv = document.getElementById("channelCountsRow");
  1599. if (cDiv) cDiv.innerHTML = "";
  1600. return;
  1601. }
  1602. const total = f.results.length;
  1603. const report = f.results.filter(r => !r.anomaly && !isItemDiscarded(r)).length;
  1604. const discard = f.results.filter(r => !r.anomaly && isItemDiscarded(r)).length;
  1605. const failed = f.results.filter(r => r.anomaly).length;
  1606. div.innerHTML = `
  1607. <div class="stat"><strong>${total}</strong><span>平台抓取总数</span></div>
  1608. <div class="stat" style="color:var(--mint)"><strong>${report}</strong><span>采纳上报案例</span></div>
  1609. <div class="stat" style="color:var(--amber)"><strong>${discard}</strong><span>不符过滤弃用</span></div>
  1610. <div class="stat" style="color:var(--rose)"><strong>${failed}</strong><span>评估失败/异常</span></div>
  1611. `;
  1612. updateChannelCounts();
  1613. }
  1614. function renderGrid() {
  1615. const f = curForm();
  1616. const grid = document.getElementById("grid");
  1617. if (!f) { grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;color:#999;padding:40px 0;">暂无数据</div>`; return; }
  1618. // Filter platforms checkboxes and render buttons
  1619. const platforms = Array.from(new Set(f.results.map(r => r.platformKey)));
  1620. const filterWrap = document.getElementById("platformFilterWrap");
  1621. let pFilterHtml = `<button class="btn ${st.channel === 'all' ? 'active' : ''}" onclick="setChannel('all')">全部渠道 (${f.results.length})</button>`;
  1622. platforms.forEach(p => {
  1623. const count = f.results.filter(r => r.platformKey === p).length;
  1624. pFilterHtml += `<button class="btn ${st.channel === p ? 'active' : ''}" onclick="setChannel('${p}')">${PLATC[p] || p} (${count})</button>`;
  1625. });
  1626. filterWrap.innerHTML = pFilterHtml;
  1627. let list = f.results;
  1628. if (st.channel !== "all") {
  1629. list = list.filter(r => r.platformKey === st.channel);
  1630. }
  1631. // Sort
  1632. const sortBy = document.getElementById("sort").value;
  1633. if (sortBy === "score") {
  1634. list.sort((a, b) => (b.overall || 0) - (a.overall || 0));
  1635. } else if (sortBy === "date") {
  1636. list.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
  1637. } else if (sortBy === "platform") {
  1638. list.sort((a, b) => (a.platformKey || "").localeCompare(b.platformKey || ""));
  1639. }
  1640. if (list.length === 0) {
  1641. grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;color:#999;padding:40px 0;">无筛选结果</div>`;
  1642. return;
  1643. }
  1644. grid.innerHTML = list.map((it, i) => {
  1645. const idx = f.results.indexOf(it);
  1646. const isDiscard = isItemDiscarded(it);
  1647. const overallStr = it.overall !== null && it.overall !== undefined ? it.overall.toFixed(1) : '—';
  1648. const discardOverlay = (isDiscard && !it.anomaly) ? `
  1649. <div class="discard-overlay">
  1650. <div class="discard-badge">已过滤</div>
  1651. <div class="discard-reason">${esc(it.reason || "时效性或综合质量均分未达标")}</div>
  1652. </div>
  1653. ` : '';
  1654. const thumbs = it.images && it.images.length > 0 ? `
  1655. <div class="thumbs">
  1656. ${it.images.map(img => `<img src="${esc(img)}" referrerpolicy="no-referrer" onerror="this.src='${NOIMG}'">`).join('')}
  1657. </div>
  1658. ` : '';
  1659. return `
  1660. <div class="result ${isDiscard ? 'discard' : ''}" data-idx="${idx}">
  1661. ${discardOverlay}
  1662. ${thumbs}
  1663. <div class="body">
  1664. <div class="meta">
  1665. <span class="platform p-${it.platformKey}">${esc(it.platform)}</span>
  1666. <span>${esc(it.date)}</span>
  1667. </div>
  1668. <h2 title="${esc(it.title)}">${esc(it.title)}</h2>
  1669. <div class="excerpt">${esc(it.text)}</div>
  1670. <div class="tags">
  1671. ${(it.tools || []).map(t => `<span class="tag">${esc(t)}</span>`).join('')}
  1672. </div>
  1673. <div class="scorebar">
  1674. <div class="overall">
  1675. <span class="decision ${isDiscard ? 'discard' : ''}">${isDiscard ? '已过滤' : '上报'}</span>
  1676. <span class="score" style="color: ${it.anomaly ? 'var(--rose)' : '#2563eb'}">${overallStr}</span>
  1677. </div>
  1678. <div class="group-snapshot">
  1679. ${groupSnapshot(it)}
  1680. </div>
  1681. <div class="actions">
  1682. <a href="${esc(it.url)}" target="_blank" onclick="event.stopPropagation()">🌐 源链接</a>
  1683. <button onclick="event.stopPropagation(); showDetail(${idx})">🔍 查看详情</button>
  1684. </div>
  1685. </div>
  1686. </div>
  1687. </div>
  1688. `;
  1689. }).join('');
  1690. }
  1691. function setChannel(ch) {
  1692. st.channel = ch;
  1693. renderGrid();
  1694. }
  1695. // Modal dialogue script
  1696. function esc(s) {
  1697. return (s === undefined || s === null ? "" : String(s)).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
  1698. }
  1699. const PLATC = { xhs: "小红书", gzh: "公众号", zhihu: "知乎", x: "X", bili: "B站", douyin: "抖音", sph: "视频号", youtube: "YouTube", github: "GitHub", toutiao: "头条", weibo: "微博" };
  1700. 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";
  1701. const scoreGroupsNew = [
  1702. { id: "relevance", title: "相关性", short: "相关", keys: ["relevance_production", "relevance_query"] },
  1703. { id: "fixed", title: "固定维度", short: "固定", keys: ["recency", "popularity", "feedback"] },
  1704. { id: "usecase", title: "用例维度", short: "用例", keys: ["realism", "expressiveness"] },
  1705. { id: "dynamic", title: "动态维度", short: "动态", keys: [] }
  1706. ];
  1707. function makeModalRow(label, scoreKey, it) {
  1708. const rawV = it.scores[scoreKey];
  1709. const v = (rawV !== undefined && rawV !== null) ? parseFloat(rawV) : NaN;
  1710. const hasScore = !isNaN(v);
  1711. const valStr = hasScore ? (Number.isInteger(v) ? v : v.toFixed(1)) : '-';
  1712. const barV = hasScore ? v : 0;
  1713. const reason = it.score_reasons ? it.score_reasons[scoreKey] : '';
  1714. const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(label)}', '${esc(scoreKey)}')" title="评判理由: ${esc(reason)}" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7;">ⓘ</span>` : '';
  1715. return `
  1716. <div class="sc-row ${!hasScore ? 'missing' : ''}">
  1717. <span class="label">${esc(label)}</span>
  1718. <div class="bar-wrap">
  1719. <div class="bar">
  1720. <div class="bar-fill" style="--v: ${barV}"></div>
  1721. </div>
  1722. <span class="value">${valStr}</span>
  1723. ${infoIcon}
  1724. </div>
  1725. </div>
  1726. `;
  1727. }
  1728. function pinScoreReason(el, label, key) {
  1729. alert(`${label} 判定理由:\n\n${el.title.replace("评判理由: ", "")}`);
  1730. }
  1731. function groupAverage(it, g) {
  1732. if (!it.scores) return null;
  1733. let keys = g.keys;
  1734. if (g.id === "dynamic") {
  1735. keys = [];
  1736. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1737. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1738. }
  1739. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1740. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1741. }
  1742. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1743. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1744. }
  1745. }
  1746. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1747. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1748. }
  1749. function getQualityAverage(it) {
  1750. if (!it.scores) return null;
  1751. const keys = ["recency", "popularity", "feedback", "realism", "expressiveness"];
  1752. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1753. keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
  1754. }
  1755. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1756. keys.push("step_input", "step_implementation", "step_output", "step_generality");
  1757. }
  1758. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1759. keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
  1760. }
  1761. const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
  1762. return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
  1763. }
  1764. function fmt(v) {
  1765. return v === null ? "N/A" : v.toFixed(1);
  1766. }
  1767. function groupSnapshot(it) {
  1768. const relAvg = groupAverage(it, scoreGroupsNew[0]);
  1769. const qualAvg = getQualityAverage(it);
  1770. return `
  1771. <div class="group-pill"><span>相关度</span><strong>${fmt(relAvg)}</strong></div>
  1772. <div class="group-pill"><span>制作质量</span><strong>${fmt(qualAvg)}</strong></div>
  1773. `;
  1774. }
  1775. function renderNewScores(it) {
  1776. const relAvg = groupAverage(it, scoreGroupsNew[0]);
  1777. const relAvgStr = relAvg !== null ? relAvg.toFixed(1) : 'N/A';
  1778. let relevanceHtml = `
  1779. <div class="sc-card">
  1780. <div class="sc-card-head">
  1781. <div class="title"><span class="badge">01</span>相关性</div>
  1782. <div class="avg-score">均分 <strong>${relAvgStr}</strong><span style="font-size:12px;color:#9ca3af;">/10</span></div>
  1783. </div>
  1784. <div class="sc-card-body">
  1785. ${makeModalRow("和内容制作知识相关", "relevance_production", it)}
  1786. ${makeModalRow("和 query 相关", "relevance_query", it)}
  1787. </div>
  1788. </div>
  1789. `;
  1790. const qualAvg = getQualityAverage(it);
  1791. const qualAvgStr = qualAvg !== null ? qualAvg.toFixed(1) : 'N/A';
  1792. let qualityHtml = `
  1793. <div class="sc-card">
  1794. <div class="sc-card-head">
  1795. <div class="title"><span class="badge">02</span>质量</div>
  1796. <div class="avg-score">均分 <strong>${qualAvgStr}</strong><span style="font-size:12px;color:#9ca3af;">/10</span></div>
  1797. </div>
  1798. <div class="sc-card-body">
  1799. <div class="sc-sub-header">固定维度</div>
  1800. ${makeModalRow("时效性", "recency", it)}
  1801. ${makeModalRow("热度性", "popularity", it)}
  1802. ${makeModalRow("评论反馈", "feedback", it)}
  1803. <div class="sc-sub-header">用例</div>
  1804. ${makeModalRow("真实感 (非AI)", "realism", it)}
  1805. ${makeModalRow("表现力", "expressiveness", it)}
  1806. `;
  1807. let dynamicHtml = '';
  1808. if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
  1809. dynamicHtml += `
  1810. <div class="sc-sub-header">工序</div>
  1811. ${makeModalRow("流程完整性", "procedure_completeness", it)}
  1812. ${makeModalRow("输入完整性", "procedure_input", it)}
  1813. ${makeModalRow("实现完整性", "procedure_implementation", it)}
  1814. ${makeModalRow("输出完整性", "procedure_output", it)}
  1815. ${makeModalRow("泛化性", "procedure_generality", it)}
  1816. `;
  1817. }
  1818. if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
  1819. dynamicHtml += `
  1820. <div class="sc-sub-header">能力</div>
  1821. ${makeModalRow("输入完整性", "step_input", it)}
  1822. ${makeModalRow("实现完整性", "step_implementation", it)}
  1823. ${makeModalRow("输出完整性", "step_output", it)}
  1824. ${makeModalRow("泛化性", "step_generality", it)}
  1825. `;
  1826. }
  1827. if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
  1828. dynamicHtml += `
  1829. <div class="sc-sub-header">工具</div>
  1830. ${makeModalRow("能力边界覆盖", "tool_boundary", it)}
  1831. ${makeModalRow("有效比较", "tool_comparison", it)}
  1832. ${makeModalRow("参数/接口具体性", "tool_specificity", it)}
  1833. ${makeModalRow("实操示例", "tool_example", it)}
  1834. ${makeModalRow("版本&限制", "tool_limits", it)}
  1835. `;
  1836. }
  1837. qualityHtml += dynamicHtml + `
  1838. </div>
  1839. </div>
  1840. `;
  1841. return relevanceHtml + qualityHtml;
  1842. }
  1843. let detailDialogIndex = -1;
  1844. function showDetail(idx) {
  1845. detailDialogIndex = idx;
  1846. const f = curForm();
  1847. const it = f.results[idx];
  1848. const isDiscard = isItemDiscarded(it);
  1849. const overallStr = it.overall !== null && it.overall !== undefined ? it.overall.toFixed(1) : '—';
  1850. document.getElementById("modalMeta").innerHTML = `
  1851. <span class="platform p-${it.platformKey}">${esc(it.platform)}</span>
  1852. <span>发布日期: ${esc(it.date)}</span>
  1853. <span style="margin-left:auto; font-family: monospace;">CaseID: ${esc(it.case_id)}</span>
  1854. `;
  1855. document.getElementById("modalTitle").textContent = it.title;
  1856. document.getElementById("modalReason").textContent = it.reason || "时效与品质良好";
  1857. document.getElementById("modalText").textContent = it.text;
  1858. document.getElementById("modalOverallScoreVal").textContent = overallStr;
  1859. document.getElementById("modalOverallScoreVal").style.color = it.anomaly ? "var(--rose)" : "#2563eb";
  1860. document.getElementById("modalScores").innerHTML = renderNewScores(it);
  1861. const tags = (it.tools || []).map(t => `<span class="tag">${esc(t)}</span>`).join("");
  1862. const foundBy = (it.found_by || []).map(q => `<span class="tag" style="background:#e0f2fe; color:#0369a1;">${esc(q)}</span>`).join("");
  1863. document.getElementById("modalTags").innerHTML = tags + foundBy;
  1864. const imgDiv = document.getElementById("modalImages");
  1865. imgDiv.innerHTML = "";
  1866. if (it.images && it.images.length > 0) {
  1867. it.images.forEach(img => {
  1868. imgDiv.innerHTML += `<img src="${esc(img)}" referrerpolicy="no-referrer" onclick="window.open(this.src)" style="cursor:zoom-in;">`;
  1869. });
  1870. } else {
  1871. imgDiv.innerHTML = `<div style="grid-column:1/-1; color:#999; font-size:13px;">无图片</div>`;
  1872. }
  1873. const tabs = document.getElementById("modalTabs");
  1874. tabs.style.display = "flex";
  1875. switchModalTab("detail");
  1876. const dialog = document.getElementById("detailDialog");
  1877. dialog.classList.remove("fullscreen");
  1878. document.getElementById("toggleFullscreenBtn").textContent = "📺 全屏";
  1879. dialog.showModal();
  1880. }
  1881. function switchModalTab(tab) {
  1882. const dBtn = document.getElementById("tabDetailBtn");
  1883. const pBtn = document.getElementById("tabProcedureBtn");
  1884. const dContent = document.getElementById("modalContentDetail");
  1885. const pContent = document.getElementById("modalContentProcedure");
  1886. if (tab === "detail") {
  1887. dBtn.classList.add("active");
  1888. pBtn.classList.remove("active");
  1889. dContent.style.display = "grid";
  1890. pContent.style.display = "none";
  1891. } else {
  1892. pBtn.classList.add("active");
  1893. dBtn.classList.remove("active");
  1894. dContent.style.display = "none";
  1895. pContent.style.display = "flex";
  1896. loadProcedureState();
  1897. }
  1898. }
  1899. function toggleModalFullscreen() {
  1900. const dialog = document.getElementById("detailDialog");
  1901. const btn = document.getElementById("toggleFullscreenBtn");
  1902. if (dialog.classList.contains("fullscreen")) {
  1903. dialog.classList.remove("fullscreen");
  1904. btn.textContent = "📺 全屏";
  1905. } else {
  1906. dialog.classList.add("fullscreen");
  1907. btn.textContent = "📺 退出全屏";
  1908. }
  1909. }
  1910. // Procedure workflow extraction & terminal logger
  1911. function loadProcedureState() {
  1912. if (procPollInterval) clearInterval(procPollInterval);
  1913. const f = curForm();
  1914. const it = f.results[detailDialogIndex];
  1915. const q = DATA.queries[st.qi].key;
  1916. const form = "A";
  1917. const caseId = it.case_id;
  1918. const setupPanel = document.getElementById("procSetupPanel");
  1919. const consolePanel = document.getElementById("procConsolePanel");
  1920. const iframe = document.getElementById("procedureIframe");
  1921. const statusText = document.getElementById("procStatusText");
  1922. const actionBtns = document.getElementById("procActionBtns");
  1923. setupPanel.style.display = "none";
  1924. consolePanel.style.display = "none";
  1925. iframe.style.display = "none";
  1926. actionBtns.innerHTML = "";
  1927. statusText.textContent = "正在检测工序状态...";
  1928. fetch(`/api/procedure_status?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
  1929. .then(r => r.json())
  1930. .then(d => {
  1931. if (d.status === "success") {
  1932. statusText.textContent = "工序状态: 已提取";
  1933. iframe.src = "/" + d.procedure_html;
  1934. iframe.style.display = "block";
  1935. actionBtns.innerHTML = `
  1936. <button class="btn" onclick="showProcSetupPanel()">♻️ 重新生成工序</button>
  1937. <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
  1938. `;
  1939. } else if (d.status === "running") {
  1940. statusText.textContent = "工序状态: 正在提取中...";
  1941. consolePanel.style.display = "flex";
  1942. pollProcLog(q, form, caseId);
  1943. } else if (d.status === "failed") {
  1944. statusText.textContent = "工序状态: 提取失败";
  1945. consolePanel.style.display = "flex";
  1946. document.getElementById("procConsoleOutput").textContent = d.error || "提取进程异常退出。";
  1947. actionBtns.innerHTML = `
  1948. <button class="btn" onclick="showProcSetupPanel()">♻️ 重试提取</button>
  1949. <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
  1950. `;
  1951. } else {
  1952. // not_started
  1953. statusText.textContent = "工序状态: 未提取";
  1954. showProcSetupPanel();
  1955. }
  1956. });
  1957. }
  1958. function showProcSetupPanel() {
  1959. document.getElementById("procSetupPanel").style.display = "flex";
  1960. document.getElementById("procConsolePanel").style.display = "none";
  1961. document.getElementById("procedureIframe").style.display = "none";
  1962. const engine = document.getElementById("procEngineSelect").value;
  1963. populateModels(engine);
  1964. }
  1965. const MODELS_BY_ENGINE = {
  1966. cyber_runner: [
  1967. "google/gemini-3.1-flash-lite",
  1968. "google/gemini-3.5-flash",
  1969. "anthropic/claude-3.5-haiku",
  1970. "anthropic/claude-3.5-sonnet",
  1971. "qwen/qwen-2.5-72b-instruct"
  1972. ],
  1973. claude_sdk: [
  1974. "claude-3-5-sonnet-20241022",
  1975. "claude-3-5-haiku-20241022"
  1976. ]
  1977. };
  1978. function populateModels(engine) {
  1979. const select = document.getElementById("procModelSelect");
  1980. const models = MODELS_BY_ENGINE[engine] || [];
  1981. select.innerHTML = models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join("");
  1982. }
  1983. function onProcEngineChange() {
  1984. const engine = document.getElementById("procEngineSelect").value;
  1985. populateModels(engine);
  1986. }
  1987. function startProcedureExtraction() {
  1988. const f = curForm();
  1989. const it = f.results[detailDialogIndex];
  1990. const q = DATA.queries[st.qi].key;
  1991. const form = "A";
  1992. const caseId = it.case_id;
  1993. const engine = document.getElementById("procEngineSelect").value;
  1994. const model = document.getElementById("procModelSelect").value;
  1995. document.getElementById("procSetupPanel").style.display = "none";
  1996. document.getElementById("procConsolePanel").style.display = "flex";
  1997. document.getElementById("procStatusText").textContent = "工序状态: 启动提取任务...";
  1998. fetch("/api/generate_procedure", {
  1999. method: "POST",
  2000. headers: { "Content-Type": "application/json" },
  2001. body: JSON.stringify({ q, form, case_id: caseId, engine, model })
  2002. })
  2003. .then(r => r.json())
  2004. .then(d => {
  2005. if (d.status === "started") {
  2006. pollProcLog(q, form, caseId);
  2007. } else {
  2008. alert("提取任务启动失败: " + d.error);
  2009. showProcSetupPanel();
  2010. }
  2011. })
  2012. .catch(err => {
  2013. alert("网络请求出错: " + err);
  2014. showProcSetupPanel();
  2015. });
  2016. }
  2017. function pollProcLog(q, form, caseId) {
  2018. if (procPollInterval) clearInterval(procPollInterval);
  2019. const consoleOutput = document.getElementById("procConsoleOutput");
  2020. const statusText = document.getElementById("procStatusText");
  2021. const consoleStatus = document.getElementById("procConsoleStatus");
  2022. consoleStatus.textContent = "streaming";
  2023. consoleStatus.style.color = "#00ff00";
  2024. const poll = () => {
  2025. fetch(`/api/procedure_status?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
  2026. .then(r => r.json())
  2027. .then(d => {
  2028. if (d.status === "success") {
  2029. clearInterval(procPollInterval);
  2030. procPollInterval = null;
  2031. loadProcedureState();
  2032. } else if (d.status === "failed") {
  2033. clearInterval(procPollInterval);
  2034. procPollInterval = null;
  2035. statusText.textContent = "工序状态: 提取失败";
  2036. consoleOutput.textContent += `\n❌ 进程异常退出: ${d.error || "未知原因"}`;
  2037. consoleStatus.textContent = "failed";
  2038. consoleStatus.style.color = "var(--rose)";
  2039. document.getElementById("procActionBtns").innerHTML = `
  2040. <button class="btn" onclick="showProcSetupPanel()">♻️ 重试提取</button>
  2041. <button class="btn" onclick="openProcLog('${esc(q)}', '${form}', '${esc(caseId)}')">📋 提取日志</button>
  2042. `;
  2043. }
  2044. });
  2045. fetch(`/api/procedure_log?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
  2046. .then(r => r.json())
  2047. .then(d => {
  2048. if (d.log) {
  2049. consoleOutput.textContent = d.log;
  2050. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2051. }
  2052. });
  2053. };
  2054. poll();
  2055. procPollInterval = setInterval(poll, 1500);
  2056. }
  2057. function openProcLog(q, form, caseId) {
  2058. const win = window.open("", "_blank");
  2059. win.document.write("<h3>加载日志中...</h3>");
  2060. fetch(`/api/procedure_log?q=${encodeURIComponent(q)}&form=${form}&case_id=${encodeURIComponent(caseId)}`)
  2061. .then(r => r.json())
  2062. .then(d => {
  2063. win.document.body.innerHTML = "";
  2064. const pre = win.document.createElement("pre");
  2065. pre.style.whiteSpace = "pre-wrap";
  2066. pre.style.fontFamily = "monospace";
  2067. pre.textContent = d.log || "无日志输出";
  2068. win.document.body.appendChild(pre);
  2069. })
  2070. .catch(err => {
  2071. win.document.body.innerHTML = "<h3>日志加载出错: " + err + "</h3>";
  2072. });
  2073. }
  2074. // Spec prompt editor script
  2075. const ALLOWED_SPEC_FILES = [
  2076. "README.md",
  2077. "tools.md",
  2078. "extraction/phase1-skeleton.md",
  2079. "extraction/phase2-normalize.md",
  2080. "extraction/phase3-finalize.md",
  2081. "taxonomy/type_suggestions.md"
  2082. ];
  2083. function openSpecEditor() {
  2084. const select = document.getElementById("specFileSelect");
  2085. select.innerHTML = ALLOWED_SPEC_FILES.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
  2086. document.getElementById("specLoadStatus").textContent = "";
  2087. document.getElementById("specSaveStatus").textContent = "";
  2088. document.getElementById("specContentTextarea").value = "";
  2089. const dialog = document.getElementById("specEditorDialog");
  2090. dialog.showModal();
  2091. loadSpecFileContent();
  2092. }
  2093. function loadSpecFileContent() {
  2094. const file = document.getElementById("specFileSelect").value;
  2095. const status = document.getElementById("specLoadStatus");
  2096. const textarea = document.getElementById("specContentTextarea");
  2097. status.textContent = "⏳ 正在读取...";
  2098. status.style.color = "var(--amber)";
  2099. textarea.disabled = true;
  2100. fetch(`/api/spec_content?file=${encodeURIComponent(file)}`)
  2101. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  2102. .then(({ ok, d }) => {
  2103. textarea.disabled = false;
  2104. if (ok) {
  2105. textarea.value = d.content || "";
  2106. status.textContent = "✓ 读取成功";
  2107. status.style.color = "var(--mint)";
  2108. } else {
  2109. status.textContent = "❌ 读取失败: " + (d.error || "未知错误");
  2110. status.style.color = "var(--rose)";
  2111. }
  2112. })
  2113. .catch(err => {
  2114. textarea.disabled = false;
  2115. status.textContent = "❌ 读取失败: " + err;
  2116. status.style.color = "var(--rose)";
  2117. });
  2118. }
  2119. function saveSpecFileContent() {
  2120. const file = document.getElementById("specFileSelect").value;
  2121. const content = document.getElementById("specContentTextarea").value;
  2122. const status = document.getElementById("specSaveStatus");
  2123. status.textContent = "⏳ 正在保存到磁盘...";
  2124. status.style.color = "var(--amber)";
  2125. fetch("/api/save_spec", {
  2126. method: "POST",
  2127. headers: { "Content-Type": "application/json" },
  2128. body: JSON.stringify({ file, content })
  2129. })
  2130. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  2131. .then(({ ok, d }) => {
  2132. if (ok) {
  2133. status.textContent = "✅ 保存成功!新的 Prompt 规则已在本地生效。";
  2134. status.style.color = "var(--mint)";
  2135. setTimeout(() => {
  2136. if (status.textContent.includes("保存成功")) {
  2137. status.textContent = "";
  2138. }
  2139. }, 4000);
  2140. } else {
  2141. status.textContent = "❌ 保存失败: " + (d.error || "未知错误");
  2142. status.style.color = "var(--rose)";
  2143. }
  2144. })
  2145. .catch(err => {
  2146. status.textContent = "❌ 网络错误: " + err;
  2147. status.style.color = "var(--rose)";
  2148. });
  2149. }
  2150. // Re-eval current query result cases
  2151. function reevalCurrentQuery() {
  2152. if (st.qi === -1 || !DATA.queries[st.qi]) return;
  2153. const q = DATA.queries[st.qi].key;
  2154. const btn = document.getElementById("reevalBtn");
  2155. btn.disabled = true;
  2156. btn.textContent = `♻️ 重评中 ${q}...`;
  2157. fetch("/api/reeval", {
  2158. method: "POST",
  2159. headers: { "Content-Type": "application/json" },
  2160. body: JSON.stringify({ q })
  2161. })
  2162. .then(r => r.json())
  2163. .then(d => {
  2164. if (d.status === "started") {
  2165. startReevalPolling(q);
  2166. } else {
  2167. alert("重评任务启动失败: " + d.error);
  2168. btn.disabled = false;
  2169. btn.textContent = "♻️ 重新评估当前结果";
  2170. }
  2171. })
  2172. .catch(err => {
  2173. alert("请求出错: " + err);
  2174. btn.disabled = false;
  2175. btn.textContent = "♻️ 重新评估当前结果";
  2176. });
  2177. }
  2178. function startReevalPolling(q) {
  2179. if (reevalPollIntervals[q]) return;
  2180. const btn = document.getElementById("reevalBtn");
  2181. const poll = () => {
  2182. fetch(`/api/reeval_status?q=${encodeURIComponent(q)}`)
  2183. .then(r => r.json())
  2184. .then(d => {
  2185. const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
  2186. if (d.status === "success") {
  2187. clearInterval(reevalPollIntervals[q]);
  2188. delete reevalPollIntervals[q];
  2189. if (isCurrent) {
  2190. btn.disabled = false;
  2191. btn.textContent = '♻️ 重新评估当前结果';
  2192. }
  2193. loadData(true);
  2194. alert(`Query "${q}" 重新评估完成!已自动更新本页评分展示。`);
  2195. } else if (d.status === "failed") {
  2196. clearInterval(reevalPollIntervals[q]);
  2197. delete reevalPollIntervals[q];
  2198. if (isCurrent) {
  2199. btn.disabled = false;
  2200. btn.textContent = '♻️ 重新评估当前结果';
  2201. }
  2202. alert(`Query "${q}" 重新评估失败: ${d.error}`);
  2203. } else if (d.status === "running") {
  2204. if (isCurrent) {
  2205. btn.disabled = true;
  2206. btn.textContent = `♻️ 重评中 ${q}...`;
  2207. }
  2208. }
  2209. })
  2210. .catch(err => console.error("Poll error:", err));
  2211. };
  2212. poll();
  2213. reevalPollIntervals[q] = setInterval(poll, 3000);
  2214. }
  2215. let batchProcPollInterval = null;
  2216. function batchExtractProcedures() {
  2217. if (st.qi === -1 || !DATA.queries[st.qi]) return;
  2218. const q = DATA.queries[st.qi].key;
  2219. const form = ['A', 'B', 'C'][st.fi];
  2220. const concurrencyInput = document.getElementById("batchConcurrency");
  2221. const concurrency = concurrencyInput ? parseInt(concurrencyInput.value) : 4;
  2222. const model = "google/gemini-3.1-flash-lite";
  2223. const btn = document.getElementById("batchProcBtn");
  2224. btn.disabled = true;
  2225. btn.textContent = "⚡ 提交任务...";
  2226. const execConsoleCard = document.getElementById("execConsoleCard");
  2227. if (execConsoleCard) {
  2228. execConsoleCard.style.display = "flex";
  2229. }
  2230. const consoleTitle = document.getElementById("consoleTitle");
  2231. if (consoleTitle) consoleTitle.textContent = `🚀 批量工序提取控制台 - ${q}`;
  2232. const consoleStatus = document.getElementById("consoleStatus");
  2233. if (consoleStatus) {
  2234. consoleStatus.textContent = "running";
  2235. consoleStatus.style.color = "var(--mint)";
  2236. }
  2237. const consoleOutput = document.getElementById("consoleOutput");
  2238. if (consoleOutput) consoleOutput.textContent = `[info] 正在为 Query: "${q}" (Form ${form}) 提交批量工序提取任务...\n`;
  2239. fetch("/api/batch_generate_procedure", {
  2240. method: "POST",
  2241. headers: { "Content-Type": "application/json" },
  2242. body: JSON.stringify({ q, form, concurrency, model })
  2243. })
  2244. .then(r => r.json().then(d => ({ ok: r.ok, d })))
  2245. .then(({ ok, d }) => {
  2246. if (!ok) {
  2247. btn.disabled = false;
  2248. btn.textContent = "⚡ 一键提取工序";
  2249. if (consoleStatus) {
  2250. consoleStatus.textContent = "failed";
  2251. consoleStatus.style.color = "var(--rose)";
  2252. }
  2253. if (consoleOutput) consoleOutput.textContent += `[error] 提交失败: ${d.error || "未知错误"}\n`;
  2254. alert("提交批量提取任务失败: " + (d.error || "未知错误"));
  2255. return;
  2256. }
  2257. if (consoleOutput) consoleOutput.textContent += `[info] 任务启动成功,后台 PID: ${d.pid || "N/A"}\n[info] 正在获取日志...\n`;
  2258. startBatchProcPolling(q);
  2259. })
  2260. .catch(err => {
  2261. btn.disabled = false;
  2262. btn.textContent = "⚡ 一键提取工序";
  2263. if (consoleStatus) {
  2264. consoleStatus.textContent = "failed";
  2265. consoleStatus.style.color = "var(--rose)";
  2266. }
  2267. if (consoleOutput) consoleOutput.textContent += `[error] 网络错误: ${err}\n`;
  2268. alert("网络错误: " + err);
  2269. });
  2270. }
  2271. function startBatchProcPolling(q) {
  2272. if (batchProcPollInterval) {
  2273. clearInterval(batchProcPollInterval);
  2274. }
  2275. const btn = document.getElementById("batchProcBtn");
  2276. const consoleStatus = document.getElementById("consoleStatus");
  2277. const consoleOutput = document.getElementById("consoleOutput");
  2278. const poll = () => {
  2279. fetch(`/api/batch_generate_status?q=${encodeURIComponent(q)}`)
  2280. .then(r => r.json())
  2281. .then(d => {
  2282. const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
  2283. if (d.status === "success") {
  2284. clearInterval(batchProcPollInterval);
  2285. batchProcPollInterval = null;
  2286. if (isCurrent) {
  2287. btn.disabled = false;
  2288. btn.textContent = "⚡ 一键提取工序";
  2289. if (consoleStatus) {
  2290. consoleStatus.textContent = "success";
  2291. consoleStatus.style.color = "var(--mint)";
  2292. }
  2293. }
  2294. fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
  2295. .then(r => r.json())
  2296. .then(ld => {
  2297. if (isCurrent && ld.log && consoleOutput) {
  2298. consoleOutput.textContent = ld.log + "\n🎉 批量工序提取已全部成功完成,已自动生成并编译 workflow json 和 HTML 文件!\n";
  2299. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2300. }
  2301. });
  2302. loadData(true);
  2303. } else if (d.status === "failed") {
  2304. clearInterval(batchProcPollInterval);
  2305. batchProcPollInterval = null;
  2306. if (isCurrent) {
  2307. btn.disabled = false;
  2308. btn.textContent = "⚡ 一键提取工序";
  2309. if (consoleStatus) {
  2310. consoleStatus.textContent = "failed";
  2311. consoleStatus.style.color = "var(--rose)";
  2312. }
  2313. }
  2314. fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
  2315. .then(r => r.json())
  2316. .then(ld => {
  2317. if (isCurrent && ld.log && consoleOutput) {
  2318. consoleOutput.textContent = ld.log + `\n❌ 任务执行失败: ${d.error || "未知原因"}\n`;
  2319. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2320. }
  2321. });
  2322. } else if (d.status === "running") {
  2323. if (isCurrent) {
  2324. btn.disabled = true;
  2325. btn.textContent = "⚡ 批量提取中...";
  2326. if (consoleStatus) {
  2327. consoleStatus.textContent = "running";
  2328. consoleStatus.style.color = "var(--mint)";
  2329. }
  2330. }
  2331. fetch(`/api/batch_generate_log?q=${encodeURIComponent(q)}`)
  2332. .then(r => r.json())
  2333. .then(ld => {
  2334. if (isCurrent && ld.log && consoleOutput) {
  2335. consoleOutput.textContent = ld.log;
  2336. consoleOutput.scrollTop = consoleOutput.scrollHeight;
  2337. }
  2338. });
  2339. }
  2340. })
  2341. .catch(err => console.error("Batch poll error:", err));
  2342. };
  2343. poll();
  2344. batchProcPollInterval = setInterval(poll, 2000);
  2345. }
  2346. function loadData(forceReload) {
  2347. document.getElementById("lede").textContent = "正在加载数据...";
  2348. fetch("/api/data")
  2349. .then(r => r.json())
  2350. .then(d => {
  2351. DATA = d;
  2352. document.getElementById("lede").textContent = `runs_new/ 下共找到 ${d.queries.length} 个检索到的 query。`;
  2353. updateThresholdLimits();
  2354. if (DATA.queries.length > 0) {
  2355. if (st.qi === -1 || st.qi >= DATA.queries.length) {
  2356. st.qi = 0;
  2357. }
  2358. st.fi = 0;
  2359. document.getElementById("resultsArea").style.display = "block";
  2360. } else {
  2361. st.qi = -1;
  2362. document.getElementById("resultsArea").style.display = "none";
  2363. }
  2364. renderSidebar();
  2365. renderGrid();
  2366. renderHead();
  2367. if (d.active_reevals) {
  2368. Object.keys(d.active_reevals).forEach(q => {
  2369. if (d.active_reevals[q] === "running") {
  2370. startReevalPolling(q);
  2371. }
  2372. });
  2373. }
  2374. if (d.active_batch_tasks) {
  2375. Object.keys(d.active_batch_tasks).forEach(q => {
  2376. if (d.active_batch_tasks[q] === "running") {
  2377. startBatchProcPolling(q);
  2378. }
  2379. });
  2380. }
  2381. })
  2382. .catch(err => {
  2383. document.getElementById("lede").textContent = "读取数据失败:" + err;
  2384. });
  2385. }
  2386. // App Initialization
  2387. (function init() {
  2388. // Build query builder dimension groups
  2389. const root = document.getElementById('dimensions');
  2390. DIMS.forEach(dim => {
  2391. const grp = document.createElement('div');
  2392. grp.className = 'dim-group';
  2393. grp.dataset.id = dim.id;
  2394. root.appendChild(grp);
  2395. renderDim(dim.id);
  2396. });
  2397. updateQueryPreview();
  2398. // Load scanned queries list
  2399. loadData(false);
  2400. })();
  2401. </script>
  2402. </body>
  2403. </html>