| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>搜索评估 · 案例总览</title>
- <style>
- :root {
- --ink: #24211d;
- --muted: #6f6961;
- --line: #ded8ce;
- --paper: #fbfaf7;
- --panel: #ffffff;
- --mint: #1f8a70;
- --rose: #b24b63;
- --amber: #b87918;
- --cyan: #2a6f8f;
- --soft-mint: #e9f5f0;
- --soft-rose: #f8e9ee;
- --soft-amber: #fff2d9;
- --soft-cyan: #e7f2f7;
- --shadow: 0 18px 45px rgba(41, 35, 28, .08);
- }
- * {
- box-sizing: border-box;
- }
- body {
- margin: 0;
- color: var(--ink);
- background: var(--paper);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
- line-height: 1.55;
- }
- header {
- padding: 40px 32px 22px;
- border-bottom: 1px solid var(--line);
- background: linear-gradient(180deg, #fff 0%, #fbfaf7 100%);
- }
- .wrap {
- max-width: 1220px;
- margin: 0 auto;
- }
- .eyebrow {
- display: flex;
- gap: 8px;
- align-items: center;
- color: var(--mint);
- font-size: 13px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0;
- }
- h1 {
- margin: 10px 0 10px;
- font-size: clamp(32px, 5vw, 58px);
- line-height: 1.06;
- letter-spacing: 0;
- }
- .lede {
- max-width: 860px;
- margin: 0;
- color: var(--muted);
- font-size: 17px;
- }
- .stats {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 12px;
- margin-top: 24px;
- }
- .stat {
- background: var(--panel);
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 14px;
- box-shadow: var(--shadow);
- min-height: 86px;
- }
- .stat strong {
- display: block;
- font-size: 26px;
- line-height: 1.1;
- }
- .stat span {
- color: var(--muted);
- font-size: 13px;
- }
- main {
- padding: 24px 32px 48px;
- }
- .toolbar {
- display: flex;
- gap: 10px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 18px;
- flex-wrap: wrap;
- }
- .filters {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- }
- button,
- select {
- border: 1px solid var(--line);
- background: #fff;
- color: var(--ink);
- border-radius: 8px;
- padding: 9px 12px;
- font: inherit;
- }
- button {
- cursor: pointer;
- }
- button.active {
- color: #fff;
- background: var(--ink);
- border-color: var(--ink);
- }
- select {
- min-width: 190px;
- }
- .grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 16px;
- }
- .result {
- min-height: 500px;
- background: var(--panel);
- border: 1px solid var(--line);
- border-radius: 8px;
- overflow: hidden;
- box-shadow: var(--shadow);
- display: flex;
- flex-direction: column;
- position: relative;
- transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.25s ease;
- }
- .result:hover {
- transform: translateY(-2px);
- box-shadow: 0 22px 50px rgba(41, 35, 28, .12);
- }
- .result.discard {
- border-color: rgba(178, 75, 99, 0.15);
- }
- .discard-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(251, 250, 247, 0.85);
- backdrop-filter: blur(5px);
- -webkit-backdrop-filter: blur(5px);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 20px;
- text-align: center;
- z-index: 5;
- opacity: 1;
- transition: opacity 0.25s ease;
- pointer-events: none;
- }
- .result.discard:hover .discard-overlay {
- opacity: 0;
- }
- .discard-badge {
- background: var(--soft-rose);
- color: var(--rose);
- border: 1px solid rgba(178, 75, 99, 0.25);
- padding: 6px 16px;
- border-radius: 999px;
- font-size: 13px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 1px;
- margin-bottom: 12px;
- box-shadow: 0 2px 8px rgba(178, 75, 99, 0.1);
- }
- .discard-reason {
- color: var(--muted);
- font-size: 13px;
- line-height: 1.6;
- max-width: 90%;
- margin: 0 auto;
- display: -webkit-box;
- -webkit-line-clamp: 6;
- -webkit-box-orient: vertical;
- overflow: hidden;
- font-weight: 500;
- }
- .thumbs {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 2px;
- height: 150px;
- background: #eee7dc;
- overflow: hidden;
- }
- .thumbs img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
- background: #eee7dc;
- }
- .thumbs img:first-child:nth-last-child(1) {
- grid-column: 1 / -1;
- }
- .body {
- padding: 15px;
- flex: 1;
- display: flex;
- flex-direction: column;
- }
- .meta {
- display: flex;
- justify-content: space-between;
- gap: 10px;
- color: var(--muted);
- font-size: 12px;
- margin-bottom: 8px;
- }
- .platform {
- color: #fff;
- border-radius: 999px;
- padding: 2px 8px;
- font-weight: 700;
- white-space: nowrap;
- }
- .p-xhs {
- background: var(--rose);
- }
- .p-gzh {
- background: var(--mint);
- }
- .p-x {
- background: var(--cyan);
- }
- h2 {
- margin: 0 0 8px;
- font-size: 18px;
- line-height: 1.25;
- letter-spacing: 0;
- }
- .excerpt {
- color: var(--muted);
- font-size: 13px;
- display: -webkit-box;
- -webkit-line-clamp: 5;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- .tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin: 12px 0;
- }
- .tag {
- background: #f2eee7;
- border-radius: 999px;
- padding: 3px 8px;
- font-size: 12px;
- color: #514a42;
- }
- .scorebar {
- margin-top: auto;
- }
- .overall {
- display: flex;
- align-items: end;
- justify-content: space-between;
- border-top: 1px solid var(--line);
- padding-top: 12px;
- }
- .score {
- font-size: 36px;
- line-height: .9;
- font-weight: 800;
- }
- .decision {
- color: var(--mint);
- font-weight: 800;
- }
- .decision.discard {
- color: var(--amber);
- }
- .mini-bars {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 4px;
- margin-top: 10px;
- }
- .mini-bars span {
- height: 7px;
- border-radius: 999px;
- background: #eee;
- overflow: hidden;
- position: relative;
- }
- .mini-bars span .fill {
- display: block;
- height: 100%;
- background: var(--mint);
- border-radius: 999px;
- }
- .group-snapshot {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 5px;
- margin-top: 10px;
- }
- .mini-bars.new-schema {
- grid-template-columns: repeat(2, 1fr) !important;
- }
- .group-snapshot.new-schema {
- grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
- }
- .group-pill {
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 5px 6px;
- background: #fff;
- min-width: 0;
- }
- .group-pill span {
- display: block;
- color: var(--muted);
- font-size: 11px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .group-pill strong {
- display: block;
- font-size: 14px;
- line-height: 1.1;
- }
- .actions {
- display: flex;
- gap: 8px;
- margin-top: 12px;
- }
- .actions a,
- .actions button {
- flex: 1;
- text-align: center;
- text-decoration: none;
- color: var(--ink);
- background: #fff;
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 8px 10px;
- font-size: 13px;
- }
- dialog {
- width: min(980px, calc(100vw - 28px));
- max-height: calc(100vh - 32px);
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 0;
- box-shadow: 0 28px 90px rgba(0, 0, 0, .25);
- }
- dialog::backdrop {
- background: rgba(38, 33, 27, .42);
- }
- .modal-head {
- position: sticky;
- top: 0;
- background: #fff;
- border-bottom: 1px solid var(--line);
- padding: 16px;
- z-index: 2;
- display: flex;
- justify-content: space-between;
- gap: 14px;
- align-items: start;
- }
- .modal-head h3 {
- margin: 0;
- font-size: 20px;
- line-height: 1.25;
- }
- .modal-content {
- display: grid;
- grid-template-columns: 1.1fr .9fr;
- gap: 18px;
- padding: 16px;
- }
- .modal-content > section,
- .modal-content > aside {
- min-width: 0;
- }
- .section-title {
- margin: 18px 0 8px;
- font-weight: 800;
- }
- .raw {
- white-space: pre-wrap;
- background: #faf7f1;
- border: 1px solid var(--line);
- border-radius: 8px;
- padding: 12px;
- max-height: 330px;
- overflow: auto;
- color: #3d3831;
- font-size: 13px;
- }
- .images {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 8px;
- }
- .images img {
- width: 100%;
- max-height: 260px;
- object-fit: contain;
- border: 1px solid var(--line);
- border-radius: 8px;
- background: #f1ece4;
- }
- .scores {
- display: grid;
- gap: 12px;
- }
- .score-group {
- border: 1px solid var(--line);
- border-radius: 8px;
- background: #fff;
- overflow: hidden;
- }
- .score-group-head {
- display: flex;
- justify-content: space-between;
- gap: 10px;
- align-items: center;
- padding: 10px 11px;
- background: #faf7f1;
- border-bottom: 1px solid var(--line);
- font-size: 13px;
- font-weight: 800;
- }
- .score-group-head small {
- color: var(--muted);
- font-weight: 700;
- white-space: nowrap;
- }
- .score-group-body {
- display: grid;
- gap: 8px;
- padding: 10px;
- }
- .score-row {
- display: grid;
- grid-template-columns: 128px 1fr 34px;
- gap: 10px;
- align-items: center;
- font-size: 13px;
- }
- .score-row.missing {
- color: #a39b91;
- }
- .score-row.missing .meter span {
- display: none;
- }
- .meter {
- height: 9px;
- border-radius: 999px;
- background: #eee7dc;
- overflow: hidden;
- }
- .meter span {
- display: block;
- height: 100%;
- width: calc(var(--v) * 20%);
- background: var(--rose);
- }
- .rubric-note {
- background: var(--soft-cyan);
- border-left: 4px solid var(--cyan);
- padding: 10px 12px;
- color: #254c5d;
- border-radius: 4px;
- font-size: 13px;
- }
- @media (max-width: 980px) {
- .grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .stats {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- .modal-content {
- grid-template-columns: 1fr;
- }
- }
- @media (max-width: 640px) {
- header,
- main {
- padding-left: 16px;
- padding-right: 16px;
- }
- .grid,
- .stats {
- grid-template-columns: 1fr;
- }
- .toolbar {
- align-items: stretch;
- }
- select {
- width: 100%;
- }
- .result {
- min-height: auto;
- }
- .group-snapshot {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- }
- /* Extra styles for interactive navigation & matrix */
- .stats {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 12px;
- }
- .stats .stat {
- min-width: 0;
- }
- .p-zhihu {
- background: #2a6f8f;
- }
- .p-x {
- background: #2a6f8f;
- }
- .p-bili {
- background: #b24b63;
- }
- .p-douyin {
- background: #24211d;
- }
- .p-sph {
- background: #07c160;
- }
- .p-youtube {
- background: #c4302b;
- }
- .p-github {
- background: #24292e;
- }
- .p-toutiao {
- background: #f04142;
- }
- .p-weibo {
- background: #e6162d;
- }
- .nav {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-bottom: 14px;
- }
- .navrow {
- display: flex;
- gap: 8px;
- align-items: center;
- flex-wrap: wrap;
- }
- .navlab {
- width: 54px;
- flex: 0 0 54px;
- color: var(--muted);
- font-size: 12px;
- font-weight: 700;
- text-transform: uppercase;
- }
- .navrow .tab {
- border: 1px solid var(--line);
- background: #fff;
- color: var(--ink);
- border-radius: 999px;
- padding: 6px 14px;
- font-size: 13px;
- cursor: pointer;
- }
- .navrow .tab.on {
- background: var(--ink);
- color: #fff;
- border-color: var(--ink);
- }
- .navrow .tab .q {
- font-family: ui-monospace, Menlo, monospace;
- }
- .navrow .tab small {
- color: var(--muted);
- margin-left: 4px;
- }
- .navrow .tab.on small {
- color: #cfd8dc;
- }
- #navQ .qtab {
- max-width: 100%;
- white-space: normal;
- text-align: left;
- line-height: 1.4;
- }
- .navrow .tab {
- white-space: normal;
- }
- #refresh {
- cursor: pointer;
- }
- /* 1:1 Morandi Matrix Replicated Style Rules */
- .btn {
- padding: 3px 10px;
- border-radius: 14px;
- border: 1px solid #d1d5db;
- background: #fff;
- cursor: pointer;
- font-size: .74rem;
- color: #444;
- }
- .g-form .btn.on {
- background: #4f46e5;
- border-color: #4f46e5;
- color: #fff;
- }
- .g-lens .btn.on {
- background: #be185d;
- border-color: #be185d;
- color: #fff;
- }
- .g-mod .btn.on {
- background: #2e7d32;
- border-color: #2e7d32;
- color: #fff;
- }
- .g-tool .btn.on {
- background: #c2410c;
- border-color: #c2410c;
- color: #fff;
- }
- .g-tier .btn.on {
- background: #374151;
- border-color: #374151;
- color: #fff;
- }
- .g-mxview .btn.on {
- background: var(--mint);
- border-color: var(--mint);
- color: #fff;
- }
- .mxwrap {
- max-height: 55vh;
- overflow: auto;
- border: 1px solid var(--line);
- border-radius: 0 0 8px 8px;
- margin-bottom: 12px;
- background: #fff;
- position: relative;
- }
- table#comboMx {
- border-collapse: separate;
- border-spacing: 0;
- background: #fff;
- font-size: .65rem;
- width: 100%;
- }
- table#comboMx th,
- table#comboMx td {
- border-right: 1px solid #f0f1f5;
- border-bottom: 1px solid #f0f1f5;
- text-align: center;
- }
- table#comboMx thead th {
- position: sticky;
- background: #fff;
- }
- table#comboMx thead tr.l1 th {
- top: 0;
- z-index: 11;
- background: #eef2ff;
- color: #4338ca;
- font-weight: 700;
- font-size: .72rem;
- height: 20px;
- border-bottom: 1px solid #c7d2fe;
- }
- table#comboMx thead tr.l2 th {
- top: 20px;
- z-index: 11;
- background: #f5f7ff;
- color: #6366f1;
- font-weight: 600;
- font-size: .66rem;
- height: 18px;
- border-bottom: 1px solid #e0e7ff;
- }
- table#comboMx thead tr.leaf th {
- top: 38px;
- z-index: 9;
- background: #fff;
- color: #1f2937;
- font-size: .66rem;
- min-width: 120px;
- max-width: 120px;
- height: 24px;
- padding: 3px;
- }
- table#comboMx thead .corner {
- left: 0;
- top: 0;
- z-index: 13;
- background: #fff;
- min-width: 96px;
- }
- .l1div {
- border-left: 3px solid #818cf8 !important;
- }
- .l2div {
- border-left: 1.5px solid #cbd5e8 !important;
- }
- table#comboMx tbody th.rh {
- position: sticky;
- left: 0;
- z-index: 8;
- background: #fffaf0;
- color: #92580c;
- text-align: left;
- padding: 4px 8px;
- font-weight: 600;
- white-space: nowrap;
- min-width: 96px;
- border-right: 1px solid #e6e8ef;
- }
- table#comboMx tbody tr.l1row td {
- background: #eef2ff;
- color: #4338ca;
- font-weight: 700;
- font-size: .7rem;
- text-align: left;
- padding: 3px 8px;
- position: sticky;
- left: 0;
- }
- td.cell {
- min-width: 120px;
- max-width: 120px;
- height: 30px;
- padding: 2px 5px;
- cursor: pointer;
- font-family: ui-monospace, Menlo, monospace;
- font-size: .65rem;
- color: #1f2937;
- text-align: left;
- line-height: 1.25;
- border-left: 4px solid transparent;
- overflow: hidden;
- position: relative;
- }
- td.cell:hover {
- box-shadow: inset 0 0 0 2px #f59e0b;
- }
- td.cell.t0 {
- border-left-color: #e5e7eb;
- background: #fbfbfc;
- color: #aeb3bd;
- }
- td.cell.t1 {
- border-left-color: #bbf7d0;
- }
- td.cell.t2 {
- border-left-color: #4ade80;
- background: #f3fdf6;
- }
- td.cell.t3 {
- border-left-color: #15803d;
- background: #ecfdf3;
- }
- td.cell.tNA {
- border-left-color: #fcd34d;
- background: repeating-linear-gradient(45deg, #fff, #fff 4px, #fef9ec 4px, #fef9ec 8px);
- color: #b6bac4;
- }
- td.cell.gq-cell {
- color: #15803d;
- font-weight: 600;
- }
- td.cell.rowdim {
- opacity: .26;
- }
- td.cell.hide {
- visibility: hidden;
- }
- td.cell.sel {
- outline: 2px solid var(--ink);
- outline-offset: -2px;
- font-weight: bold;
- }
- td.cell.rowsel,
- td.cell.colsel {
- box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .12);
- }
- td.cell.rowsel.colsel {
- box-shadow: inset 0 0 0 999px rgba(255, 193, 7, .22);
- }
- th.rh.rowsel {
- background: rgba(255, 193, 7, .22);
- color: var(--ink);
- font-weight: 700;
- }
- thead th.colsel {
- background: rgba(255, 193, 7, .22);
- color: var(--ink);
- font-weight: 700;
- }
- /* Database hit badge inside matrix cells */
- td.cell .hit-badge {
- position: absolute;
- right: 2px;
- top: 2px;
- background: #10b981;
- color: #fff;
- font-size: 9px;
- font-weight: bold;
- border-radius: 3px;
- padding: 0 3px;
- line-height: 1.2;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- }
- /* Pop-up floating panel styling */
- .pop {
- position: fixed;
- background: #fff;
- border: 1px solid #d1d5db;
- border-radius: 9px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
- padding: 13px 15px;
- max-width: 450px;
- z-index: 200;
- display: none;
- font-size: .82rem;
- line-height: 1.5;
- }
- .pop .pt {
- font-weight: 700;
- color: #1f2937;
- margin-bottom: 7px;
- display: flex;
- gap: 6px;
- align-items: center;
- flex-wrap: wrap;
- padding-right: 18px;
- }
- .pop .path {
- font-size: .66rem;
- padding: 1px 6px;
- border-radius: 3px;
- }
- .pop .path.a {
- background: #eef2ff;
- color: #4338ca;
- }
- .pop .path.t {
- background: #fef3c7;
- color: #92400e;
- }
- .pop .tier {
- font-size: .64rem;
- padding: 1px 7px;
- border-radius: 3px;
- font-weight: 600;
- }
- .tb0 {
- background: #e5e7eb;
- color: #6b7280;
- }
- .tb1 {
- background: #dcfce7;
- color: #166534;
- }
- .tb2 {
- background: #bbf7d0;
- color: #14532d;
- }
- .tb3 {
- background: #15803d;
- color: #fff;
- }
- .tbNA {
- background: #fef3c7;
- color: #92400e;
- }
- .pop .reason {
- background: #fffbeb;
- border-left: 3px solid #f59e0b;
- padding: 7px 11px;
- border-radius: 3px;
- color: #78350f;
- margin: 7px 0;
- font-size: .8rem;
- }
- .pop .reason.inv {
- background: #fef2f2;
- border-left-color: #ef4444;
- color: #991b1b;
- }
- .pop .gq {
- background: #ecfdf3;
- border: 1px solid #a7f3d0;
- border-radius: 6px;
- padding: 7px 11px;
- margin: 7px 0;
- font-family: ui-monospace, Menlo, monospace;
- font-size: .84rem;
- color: #15803d;
- }
- .pop .gq .t {
- font-family: inherit;
- font-size: .64rem;
- color: #6b7280;
- display: block;
- margin-bottom: 2px;
- }
- .pop .small {
- font-size: .7rem;
- color: #9aa0ad;
- margin: 4px 0;
- }
- .pop .lbl {
- font-size: .66rem;
- color: #6b7280;
- font-weight: 600;
- margin: 9px 0 3px;
- }
- .pop ul {
- margin: 0;
- padding: 0;
- list-style: none;
- }
- .pop li {
- padding: 3px 8px;
- background: #f9fafb;
- border-radius: 4px;
- margin-bottom: 3px;
- font-family: ui-monospace, Menlo, monospace;
- font-size: .78rem;
- color: #1f2937;
- }
- .pop li.gen {
- background: #eef0ff;
- color: #3730a3;
- }
- .pop li.cur {
- box-shadow: inset 0 0 0 2px #818cf8;
- }
- .pop li .fm {
- font-size: .62rem;
- color: #4f46e5;
- margin-right: 6px;
- }
- .pop .note {
- font-size: .7rem;
- color: #2e7d32;
- margin-top: 6px;
- }
- .pop .close {
- position: absolute;
- top: 6px;
- right: 9px;
- cursor: pointer;
- color: #9ca3af;
- font-size: 1.15rem;
- }
- .fac {
- border: 1px solid var(--line);
- background: #fff;
- border-radius: 999px;
- padding: 4px 11px;
- font-size: 12px;
- cursor: pointer;
- color: var(--ink);
- }
- .fac.on {
- background: var(--mint);
- color: #fff;
- border-color: var(--mint);
- }
- .fac small {
- opacity: .65;
- margin-left: 3px;
- }
- #navQ .qtab .hit {
- display: inline-block;
- margin-left: 7px;
- background: var(--soft-mint);
- color: var(--mint);
- border-radius: 999px;
- padding: 0 8px;
- font-size: 11px;
- font-weight: 700;
- }
- #navQ .qtab.on .hit {
- background: rgba(255, 255, 255, .22);
- color: #fff;
- }
- .modal-tabs {
- display: flex;
- gap: 4px;
- padding: 0 16px;
- border-bottom: 1px solid var(--line);
- background: #faf7f1;
- }
- .modal-tab {
- background: transparent;
- border: none;
- border-bottom: 3px solid transparent;
- border-radius: 0;
- padding: 10px 16px;
- font-size: 14px;
- font-weight: 600;
- color: var(--muted);
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .modal-tab:hover {
- color: var(--ink);
- background: rgba(0, 0, 0, 0.02);
- }
- .modal-tab.active {
- color: var(--mint);
- border-bottom-color: var(--mint);
- }
- /* 新版评估分数卡片可视化样式 */
- .sc-card {
- background: #fff;
- border: 1px solid var(--line);
- border-radius: 12px;
- padding: 16px 20px;
- margin-bottom: 16px;
- box-shadow: 0 4px 15px rgba(0,0,0,0.02);
- }
-
- .sc-card-head {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 14px;
- border-bottom: 1px solid #f3f0ea;
- padding-bottom: 10px;
- }
-
- .sc-card-head .title {
- font-size: 16px;
- font-weight: 700;
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .sc-card-head .badge {
- background: #eef2ff;
- color: #2563eb;
- font-size: 11px;
- padding: 2px 6px;
- border-radius: 4px;
- font-weight: 700;
- }
-
- .sc-card-head .avg-score {
- font-size: 14px;
- color: var(--muted);
- font-weight: 600;
- }
-
- .sc-card-head .avg-score strong {
- font-size: 26px;
- color: #2563eb;
- font-weight: 800;
- margin-left: 6px;
- }
-
- .sc-sub-header {
- font-size: 12px;
- color: var(--muted);
- font-weight: 700;
- margin: 14px 0 8px;
- border-bottom: 1px dashed #f0ebd8;
- padding-bottom: 4px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .sc-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 6px 0;
- font-size: 13.5px;
- gap: 10px;
- }
-
- .sc-row .label {
- color: var(--ink);
- font-weight: 500;
- flex: 1;
- min-width: 100px;
- word-break: break-all;
- }
-
- .sc-row .bar-wrap {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 170px;
- flex-shrink: 0;
- }
-
- .sc-row .bar {
- height: 6px;
- background: #eee7dc;
- border-radius: 999px;
- flex: 1;
- overflow: hidden;
- }
-
- .sc-row .bar-fill {
- height: 100%;
- background: #2563eb;
- border-radius: 999px;
- width: calc(var(--v) * 10%);
- }
-
- .sc-row .value {
- font-weight: 700;
- font-size: 13.5px;
- width: 20px;
- text-align: right;
- }
-
- .sc-row .info-icon {
- cursor: pointer;
- color: #9ca3af;
- transition: color 0.15s ease;
- font-size: 13px;
- user-select: none;
- }
-
- .sc-row .info-icon:hover {
- color: #3b82f6;
- }
- #reevalBtn {
- background: #faf7f1;
- color: var(--amber);
- border-color: rgba(184, 121, 24, 0.3);
- height: 38px;
- padding: 0 16px;
- font-size: 13px;
- font-weight: 600;
- border-radius: 8px;
- box-shadow: var(--shadow);
- }
- #reevalBtn:hover {
- background: var(--soft-amber);
- color: var(--amber);
- border-color: var(--amber);
- }
- #editSpecBtn {
- background: #f0f7f6;
- color: var(--cyan);
- border-color: rgba(42, 111, 143, 0.3);
- height: 38px;
- padding: 0 16px;
- font-size: 13px;
- font-weight: 600;
- border-radius: 8px;
- box-shadow: var(--shadow);
- }
- #editSpecBtn:hover {
- background: var(--soft-cyan);
- color: var(--cyan);
- border-color: var(--cyan);
- }
- </style>
- </head>
- <body>
- <header>
- <div class="wrap">
- <div class="eyebrow">Content Search · 固定 query · 同义扩展 + 合并去重 · query → 渠道</div>
- <h1>固定 Query 搜索评估 · 案例总览</h1>
- <p class="lede" id="lede" style="margin:0">加载中…</p>
- <div class="stats" id="stats"></div>
- </div>
- </header>
- <main>
- <div class="wrap">
- <div class="nav">
- <!-- fixed_query_eval:隐藏原正交矩阵导航,改用固定 query 选择条 #navQ。
- 矩阵 DOM 保留(display:none)以免 loadData 里的 getElementById 取空崩脚本。 -->
- <style>#mxRow, .mx-header, .mxwrap, #pop { display: none !important; }</style>
- <div class="navrow" id="navQRow" style="margin-bottom:4px;">
- <span class="navlab">Query</span>
- <div id="navQ" style="display:flex;gap:8px;flex-wrap:wrap;flex:1"></div>
- <button id="refresh2" onclick="loadData(true)">↻ 刷新 runs</button>
- </div>
- <div class="navrow" id="mxRow" style="margin-bottom:4px;">
- <span class="navlab">组合矩阵</span>
- <span style="color:var(--muted);font-size:12px;flex:1">行=类型 列=动作 格子色=评分,角标=帖子数/工序数。点击选组合且弹出详情。</span>
- <span class="g-mxview" style="display:inline-flex;gap:4px;margin-right:12px;align-items:center;">
- <button class="btn on" id="btnMxFull" onclick="setMatrixView('full')">完整矩阵</button>
- <button class="btn" id="btnMxHits" onclick="setMatrixView('hits')">只看帖子命中</button>
- <button class="btn" id="btnMxProcedures" onclick="setMatrixView('procedures')">只看工序</button>
- </span>
- <button id="refresh" onclick="loadData(true)">↻ 刷新 runs</button>
- </div>
- <div class="mx-header"
- style="background:#fff; border: 1px solid var(--line); border-radius: 8px 8px 0 0; padding:12px 16px 8px; border-bottom: none;">
- <h4
- style="margin:0 0 4px; font-size:13px; color:#1f2937; display:flex; justify-content:space-between; align-items:center; font-weight:700;">
- <span>动作 × 类型 · 组合矩阵 <span style="font-weight:400;font-size:11px;color:#9aa0ad">基于 <b
- id="gm">gemini-3.1-flash-lite</b></span></span>
- <span class="legend" style="display:flex;gap:9px;align-items:center;color:#6b7280;font-size:11px;">
- <span><span class="sw"
- style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#15803d;vertical-align:middle;margin-right:3px;"></span>高
- · <b id="s3" style="color:#15803d;">0</b></span>
- <span><span class="sw"
- style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#4ade80;vertical-align:middle;margin-right:3px;"></span>中
- · <b id="s2" style="color:#4ade80;">0</b></span>
- <span><span class="sw"
- style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#bbf7d0;vertical-align:middle;margin-right:3px;"></span>低
- · <b id="s1" style="color:#10b981;">0</b></span>
- <span><span class="sw"
- style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#e5e7eb;vertical-align:middle;margin-right:3px;"></span>无效
- · <b id="s0" style="color:#9ca3af;">0</b></span>
- <span><span class="sw"
- style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#fcd34d;vertical-align:middle;margin-right:3px;"></span>未判
- · <b id="sna" style="color:#b87918;">0</b></span>
- </span>
- </h4>
- <div class="sub" style="font-size:11px;color:#6b7280;margin-bottom:8px;">
- 共 1350 格 · 形式①原词不替换 · ②句子套模板 · ③同义池替换。
- </div>
- <div class="ctl"
- style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:11px;border-top:1px solid #f3f4f6;padding-top:8px;">
- <span class="lab" style="color:#9aa0ad;font-weight:600;">query生成方式</span>
- <span class="g-form">
- <button class="btn on" data-k="form" data-v="A">①原词</button>
- <button class="btn" data-k="form" data-v="B">②句子</button>
- <button class="btn" data-k="form" data-v="C">③同义</button>
- </span>
- <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">知识类型</span>
- <span class="g-lens">
- <button class="btn on" data-k="lens" data-v="工序">工序</button>
- <button class="btn" data-k="lens" data-v="工具">工具</button>
- <button class="btn" data-k="lens" data-v="能力">能力</button>
- </span>
- <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">约束·工具类型</span>
- <span class="g-tool">
- <button class="btn on" data-k="tool" data-v="">无</button>
- <span id="toolBtns"></span>
- </span>
- <span class="lab" style="color:#9aa0ad;font-weight:600;margin-left:8px;">搜索优先级</span>
- <span class="g-tier">
- <button class="btn on" data-k="tier" data-v="0">全部</button>
- <button class="btn" data-k="tier" data-v="2">≥中</button>
- <button class="btn" data-k="tier" data-v="3">仅高</button>
- </span>
- </div>
- </div>
- <div class="mxwrap"
- style="max-height:55vh; overflow:auto; border:1px solid var(--line); border-radius: 0 0 8px 8px; margin-bottom:12px; background:#fff; position:relative;">
- <table id="comboMx"></table>
- </div>
- <div class="pop" id="pop"></div>
- <div class="navrow"><span class="navlab">渠道</span>
- <div id="navC" style="display:flex;gap:8px;flex-wrap:wrap"></div>
- </div>
- </div>
- <div class="toolbar">
- <div class="filters"></div>
-
- <!-- 动态相关性过滤阈值 -->
- <div class="threshold-control" style="display: flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 4px 12px; font-size: 13px; font-weight: 600; box-shadow: var(--shadow);">
- <span style="color: var(--muted); font-size: 12px; font-weight: 700;">相关性过滤阈值:</span>
- <input type="number" id="relThreshold" min="0" max="10" step="0.5" value="4.0"
- oninput="renderGrid(); renderHead();"
- style="width: 55px; border: 1px solid #d1d5db; border-radius: 4px; padding: 2px 6px; font-weight: 700; text-align: center; color: #2563eb; outline: none; transition: border-color 0.2s;"
- onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#d1d5db'">
- </div>
- <button id="reevalBtn" onclick="reevalCurrentQuery()" title="只对当前 query 的所有 form/帖子复评(不重新搜索)">♻️ 重评当前 query</button>
- <button id="editSpecBtn" onclick="openSpecEditor()" title="查看或修改 spec/ 提示词规范文件">📝 编辑 Spec 提示词</button>
- <select id="sort">
- <option value="score">按综合分排序</option>
- <option value="date">按发布时间排序</option>
- <option value="platform">按平台排序</option>
- </select>
- </div>
- <div class="grid" id="grid"></div>
- </div>
- </main>
- <!-- fixed_query_eval:图片放大灯箱(用 dialog 才能叠在详情 modal 之上)-->
- <style>
- #imgLightbox { border:none; background:transparent; padding:0; width:100vw; height:100vh; max-width:100vw; max-height:100vh; overflow:hidden; }
- #imgLightbox::backdrop { background: rgba(0,0,0,.88); }
- #imgLightbox .lb-stage { width:100%; height:100%; display:flex; align-items:center; justify-content:center; position:relative; }
- #imgLightbox .lb-stage img { max-width:86vw; max-height:90vh; object-fit:contain; border-radius:8px; box-shadow:0 10px 40px rgba(0,0,0,.5); cursor:pointer; display:block; }
- #imgLightbox .lb-nav { position:absolute; top:50%; transform:translateY(-50%); width:50px; height:68px; border:none; border-radius:10px; background:rgba(255,255,255,.16); color:#fff; font-size:36px; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background .15s; user-select:none; }
- #imgLightbox .lb-nav:hover { background:rgba(255,255,255,.32); }
- #imgLightbox .lb-prev { left:26px; } #imgLightbox .lb-next { right:26px; }
- #imgLightbox .lb-nav.hidden { display:none; }
- #imgLightbox .lb-counter { position:absolute; bottom:22px; left:50%; transform:translateX(-50%); color:#fff; font-size:13px; font-weight:600; background:rgba(0,0,0,.55); padding:5px 14px; border-radius:20px; }
- #imgLightbox .lb-close { position:absolute; top:22px; right:26px; width:38px; height:38px; border:none; border-radius:50%; background:rgba(255,255,255,.16); color:#fff; font-size:18px; cursor:pointer; }
- #imgLightbox .lb-close:hover { background:rgba(255,255,255,.32); }
- /* fixed_query_eval:工具解构表格美化 + 单元格限高展开 */
- .fqe-ttwrap { overflow-x:auto; border:1px solid #e6ded2; border-radius:10px; box-shadow:0 2px 12px rgba(0,0,0,.05); }
- .fqe-tt { border-collapse:separate; border-spacing:0; width:100%; min-width:1180px; background:#fff; font-size:12.5px; }
- .fqe-tt thead th {
- position:sticky; top:0; z-index:2; text-align:left; white-space:nowrap;
- background:linear-gradient(180deg,#2aa79b,#1c8076); color:#fff; font-weight:700;
- padding:10px 12px; letter-spacing:.3px; border-right:1px solid rgba(255,255,255,.18);
- }
- .fqe-tt thead th:last-child { border-right:none; }
- .fqe-tt tbody td { padding:9px 12px; vertical-align:top; line-height:1.6;
- border-bottom:1px solid #f0eae0; border-right:1px solid #f5f0e8; color:#3a3a3a; }
- .fqe-tt tbody td:last-child { border-right:none; }
- /* 两层表头:第二行子表头下移吸顶;group/sub 表头样式 */
- .fqe-tt2 thead tr:first-child th { top:0; }
- .fqe-tt2 thead tr:nth-child(2) th { top:38px; }
- .fqe-tt .th-group { text-align:center; }
- .fqe-tt .th-sub { background:linear-gradient(180deg,#36bdb0,#23897f); font-weight:600; }
- .fqe-tt td.col-case { background:#fafdfc; }
- /* 斑马纹按「工具」分组(rowspan 安全),不用 nth-child */
- .fqe-tt tbody tr.tr-b td { background:#fbfaf6; }
- .fqe-tt tbody tr.tr-b td.col-case { background:#f6fbfa; }
- .fqe-tt tbody td.col-tool { font-weight:700; color:#176d64; white-space:nowrap;
- border-left:3px solid #2aa79b; background:#f3faf8; }
- .fqe-tt tbody tr:hover td.col-tool { background:#e3f4f0; }
- .fqe-tt ul { margin:0; padding-left:17px; }
- .fqe-tt ul li { margin:3px 0; }
- .fqe-tt ul li::marker { color:#2aa79b; }
- .fqe-tt .layer-badge { display:inline-block; font-weight:700; font-size:11px; padding:2px 10px; border-radius:20px; white-space:nowrap; }
- .fqe-tt .layer-badge.make { color:#0e7490; background:#d6f0ee; }
- .fqe-tt .layer-badge.create { color:#b8731a; background:#fef0db; }
- .fqe-tt .dash { color:#c9c2b6; }
- /* 单元格限高 + 渐变蒙版 + 点击展开 */
- .fqe-tt .tcell { position:relative; max-height:7.8em; overflow:hidden; transition:max-height .15s; }
- .fqe-tt .tcell.clamped { cursor:zoom-in; }
- .fqe-tt .tcell.clamped::after { content:'▾ 展开'; position:absolute; left:0; right:0; bottom:0;
- height:2.6em; display:flex; align-items:flex-end; justify-content:center; padding-bottom:2px;
- font-size:11px; font-weight:700; color:#176d64;
- background:linear-gradient(rgba(255,255,255,0), #fff 72%); pointer-events:none; }
- .fqe-tt tbody tr.tr-b .tcell.clamped::after { background:linear-gradient(rgba(251,250,246,0), #fbfaf6 72%); }
- .fqe-tt td.col-case .tcell.clamped::after { background:linear-gradient(rgba(250,253,252,0), #fafdfc 72%); }
- .fqe-tt tbody tr.tr-b td.col-case .tcell.clamped::after { background:linear-gradient(rgba(246,251,250,0), #f6fbfa 72%); }
- .fqe-tt .tcell.open { max-height:none; cursor:zoom-out; }
- .fqe-tt .tcell.open::after { content:''; height:0; }
- </style>
- <dialog id="imgLightbox">
- <div class="lb-stage" onclick="if(event.target===this) imgLightbox.close()">
- <button class="lb-nav lb-prev" onclick="lightboxNav(-1)" aria-label="上一张">‹</button>
- <img id="imgLightboxImg" referrerpolicy="no-referrer" alt="" onclick="lightboxNav(1)" title="点击看下一张">
- <button class="lb-nav lb-next" onclick="lightboxNav(1)" aria-label="下一张">›</button>
- <div class="lb-counter" id="lbCounter"></div>
- <button class="lb-close" onclick="imgLightbox.close()" aria-label="关闭">✕</button>
- </div>
- </dialog>
- <!-- fixed_query_eval:批量工具解构 · 选帖弹层 -->
- <dialog id="toolBatchDialog" style="width: min(720px, 92vw); border:1px solid var(--line); border-radius:12px; padding:0;">
- <div style="padding:16px 20px; border-bottom:1px solid var(--line); display:flex; justify-content:space-between; align-items:center;">
- <div>
- <h3 style="margin:0; font-size:17px;">🔧 工具解构 · 选择帖子</h3>
- <div id="toolBatchSub" style="color:var(--muted); font-size:13px; margin-top:2px;"></div>
- </div>
- <button onclick="toolBatchDialog.close()">关闭</button>
- </div>
- <div style="padding:10px 20px; border-bottom:1px solid var(--line); display:flex; gap:14px; align-items:center;">
- <label style="display:flex; gap:6px; align-items:center; cursor:pointer; font-size:13px;">
- <input type="checkbox" id="toolBatchAll" onchange="toolBatchToggleAll(this.checked)"> 全选
- </label>
- <span id="toolBatchCount" style="color:var(--muted); font-size:12px;"></span>
- <span style="flex:1"></span>
- <span style="color:var(--muted); font-size:12px;">模型:gemini-3.1-flash-lite</span>
- </div>
- <div id="toolBatchList" style="max-height:50vh; overflow-y:auto; padding:4px 20px;"></div>
- <div style="padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px;">
- <button onclick="toolBatchDialog.close()">取消</button>
- <button id="toolBatchConfirm" onclick="confirmToolExtract()" style="background:var(--mint); color:#fff; border-color:var(--mint); padding:8px 20px; font-weight:bold; border-radius:8px; cursor:pointer;">确认解构</button>
- </div>
- </dialog>
- <dialog id="detailDialog">
- <div class="modal-head">
- <div>
- <div id="modalMeta" class="meta"></div>
- <h3 id="modalTitle"></h3>
- </div>
- <button onclick="detailDialog.close()">关闭</button>
- </div>
-
- <div class="modal-tabs" id="modalTabs" style="display: none;">
- <button class="modal-tab active" onclick="switchModalTab('detail')" id="tabDetailBtn">帖子详情</button>
- <button class="modal-tab" onclick="switchModalTab('procedure')" id="tabProcedureBtn">对应工序</button>
- <button class="modal-tab" onclick="switchModalTab('tools')" id="tabToolsBtn">工具解构</button>
- </div>
- <div class="modal-content" id="modalContentDetail">
- <section>
- <div class="rubric-note" id="modalReason"></div>
- <div class="section-title">抓取文本节选</div>
- <div class="raw" id="modalText"></div>
- <div class="section-title">图片预览</div>
- <div class="images" id="modalImages"></div>
- </section>
- <aside>
- <div class="section-title" style="display: flex; justify-content: space-between; align-items: center;">
- <span>评分详情</span>
- <span id="modalOverallScore" style="font-size: 13.5px; font-weight: 700; color: var(--muted); display: flex; align-items: center; gap: 4px; background: #faf7f1; border: 1px solid var(--line); padding: 3px 10px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.01);">
- 综合评分 <strong style="font-size: 19px; color: #2563eb; font-weight: 900; line-height: 1;" id="modalOverallScoreVal">—</strong>
- </span>
- </div>
- <div class="scores" id="modalScores"></div>
- <div class="section-title">类型 / 命中 query</div>
- <div class="tags" id="modalTags"></div>
- </aside>
- </div>
- <!-- fixed_query_eval:工具解构 tab 内容(JS 填充)-->
- <div id="modalContentTools" style="display: none; min-height: 400px; max-height: calc(100vh - 180px); overflow-y: auto; padding: 16px; box-sizing: border-box;"></div>
- <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;">
- <!-- Top Action Bar (inside tab) -->
- <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;">
- <div style="font-size: 14px; font-weight: bold; color: var(--ink);" id="procStatusText">工序状态: 检测中...</div>
- <div style="display: flex; gap: 8px;" id="procActionBtns">
- <!-- Dynamically populated buttons (Regenerate, View Logs, etc) -->
- </div>
- </div>
-
- <!-- Main view container -->
- <div style="flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column;">
- <!-- Configuration / Setup panel (when not generated) -->
- <div id="procSetupPanel" style="display: none; flex-direction: column; align-items: center; justify-content: center; text-align: center; height: 100%; padding: 40px 20px;">
- <div style="font-size: 36px; margin-bottom: 16px;">✨</div>
- <h4 style="margin: 0 0 10px; font-size: 18px;">提取本帖工序</h4>
- <p style="color: var(--muted); font-size: 14px; max-width: 500px; margin: 0 0 24px;">该帖子目前尚未生成对应的结构化工序。请在下方选择提取引擎和模型,点击开始提取。</p>
-
- <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);">
- <div>
- <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">提取引擎 (Engine)</label>
- <select id="procEngineSelect" style="min-width: 180px; padding: 6px 10px;" onchange="onProcEngineChange()">
- <option value="cyber_runner">Cyber Runner (自研/OpenRouter)</option>
- <option value="claude_sdk">Claude SDK (OAuth)</option>
- </select>
- </div>
- <div>
- <label style="display: block; font-size: 12px; font-weight: bold; margin-bottom: 6px; color: var(--muted);">AI 模型 (Model)</label>
- <select id="procModelSelect" style="min-width: 240px; padding: 6px 10px;">
- <!-- Dynamically populated options -->
- </select>
- </div>
- </div>
-
- <button id="startProcBtn" onclick="startProcedureExtraction()" style="background: var(--mint); color: #fff; border-color: var(--mint); padding: 10px 24px; font-size: 15px; font-weight: bold; border-radius: 8px; cursor: pointer; transition: all 0.2s;">开始提取工序</button>
- </div>
-
- <!-- Live Log Terminal View -->
- <div id="procConsolePanel" style="display: none; flex-direction: column; height: 100%; min-height: 0; background: #1e1b18; border: 1px solid #3d352e; border-radius: 8px; overflow: hidden; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5);">
- <!-- Console header -->
- <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;">
- <span>TERMINAL CONSOLE</span>
- <span id="procConsoleStatus">idle</span>
- </div>
- <!-- Console log output -->
- <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>
- </div>
-
- <!-- Iframe Panel for completed HTML -->
- <iframe id="procedureIframe" style="display: none; width: 100%; height: 100%; border: 1px solid var(--line); border-radius: 8px; background: #fff;" referrerpolicy="no-referrer"></iframe>
- </div>
- </div>
- </dialog>
- <!-- Spec Editor Dialog -->
- <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);">
- <div style="display: flex; justify-content: space-between; align-items: center; background: #faf7f1; border-bottom: 1px solid var(--line); padding: 16px 20px;">
- <h3 style="margin: 0; font-size: 18px; color: var(--ink); font-weight: 800;">📝 编辑 Spec 提示词规范</h3>
- <button onclick="document.getElementById('specEditorDialog').close()" style="background: none; border: 1px solid var(--line); padding: 5px 14px; border-radius: 8px; cursor: pointer; color: var(--muted); font-weight: 600; font-size: 12px;">关闭</button>
- </div>
- <div style="padding: 20px;">
- <div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
- <span style="font-size: 13.5px; font-weight: 700; color: var(--ink);">选择提示词文件:</span>
- <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;">
- <!-- Spec files options will be populated -->
- </select>
- <span id="specLoadStatus" style="font-size: 13px; font-weight: 600;"></span>
- </div>
- <div style="position: relative; margin-bottom: 20px;">
- <textarea id="specContentTextarea" style="width: 100%; height: 500px; font-family: ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.55; padding: 14px; border: 1px solid #d1d5db; border-radius: 8px; box-sizing: border-box; outline: none; background: #fafaf9; color: #1f2937; resize: vertical; border-left: 4px solid var(--cyan); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);"></textarea>
- </div>
- <div style="display: flex; justify-content: flex-end; gap: 12px; align-items: center; border-top: 1px solid var(--line); padding-top: 16px;">
- <span id="specSaveStatus" style="font-size: 13.5px; font-weight: 700; margin-right: auto;"></span>
- <button onclick="document.getElementById('specEditorDialog').close()" class="btn" style="background: #f3f4f6; height: 38px; padding: 0 18px; border-radius: 8px; font-weight: 600;">取消</button>
- <button onclick="saveSpecFileContent()" class="btn" style="background: var(--mint); color: #fff; border-color: var(--mint); font-weight: bold; height: 38px; padding: 0 20px; border-radius: 8px; box-shadow: var(--shadow);">💾 保存修改</button>
- </div>
- </div>
- </dialog>
- <script>
- let DATA = { queries: [], actions: [], types: [], matrix: [] }, st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full' }, VIEW = [];
- let currentProcTask = null;
- let procPollInterval = null;
- let isLogViewActive = false;
- let reevalPollIntervals = {};
- function startReevalPolling(q) {
- if (reevalPollIntervals[q]) return;
- const btn = document.getElementById('reevalBtn');
-
- const poll = () => {
- fetch(`/api/reeval_status?q=${q}`)
- .then(r => r.json())
- .then(d => {
- const isCurrent = DATA.queries[st.qi] && DATA.queries[st.qi].key === q;
- if (d.status === "success") {
- clearInterval(reevalPollIntervals[q]);
- delete reevalPollIntervals[q];
- if (isCurrent) {
- btn.disabled = false;
- btn.textContent = '♻️ 重评当前 query';
- }
- // Mark local status as finished
- if (DATA.active_reevals) {
- delete DATA.active_reevals[q];
- }
- loadData(true);
- alert(`Query ${q} 重评完成!已自动重新扫描并更新数据。`);
- } else if (d.status === "failed") {
- clearInterval(reevalPollIntervals[q]);
- delete reevalPollIntervals[q];
- if (isCurrent) {
- btn.disabled = false;
- btn.textContent = '♻️ 重评当前 query';
- }
- if (DATA.active_reevals) {
- delete DATA.active_reevals[q];
- }
- alert(`Query ${q} 重评失败:${d.error}`);
- } else if (d.status === "running") {
- if (isCurrent) {
- btn.disabled = true;
- btn.textContent = `♻️ 重评中 ${q}...`;
- }
- }
- })
- .catch(err => console.error("Poll error:", err));
- };
-
- poll();
- reevalPollIntervals[q] = setInterval(poll, 3000);
- }
- // Spec Prompt Editor Functions
- const ALLOWED_SPEC_FILES = [
- "README.md",
- "tools.md",
- "extraction/phase1-skeleton.md",
- "extraction/phase2-normalize.md",
- "extraction/phase3-finalize.md",
- "taxonomy/type_suggestions.md"
- ];
- function openSpecEditor() {
- const select = document.getElementById("specFileSelect");
- select.innerHTML = ALLOWED_SPEC_FILES.map(f => `<option value="${esc(f)}">${esc(f)}</option>`).join("");
-
- document.getElementById("specLoadStatus").textContent = "";
- document.getElementById("specSaveStatus").textContent = "";
- document.getElementById("specContentTextarea").value = "";
-
- const dialog = document.getElementById("specEditorDialog");
- dialog.showModal();
- loadSpecFileContent();
- }
- function loadSpecFileContent() {
- const file = document.getElementById("specFileSelect").value;
- const status = document.getElementById("specLoadStatus");
- const textarea = document.getElementById("specContentTextarea");
-
- status.textContent = "⏳ 正在读取...";
- status.style.color = "var(--amber)";
- textarea.disabled = true;
-
- fetch(`/api/spec_content?file=${encodeURIComponent(file)}`)
- .then(r => r.json().then(d => ({ ok: r.ok, d })))
- .then(({ ok, d }) => {
- textarea.disabled = false;
- if (ok) {
- textarea.value = d.content || "";
- status.textContent = "✓ 读取成功";
- status.style.color = "var(--mint)";
- } else {
- status.textContent = "❌ 读取失败: " + (d.error || "未知错误");
- status.style.color = "var(--rose)";
- }
- })
- .catch(err => {
- textarea.disabled = false;
- status.textContent = "❌ 读取失败: " + err;
- status.style.color = "var(--rose)";
- });
- }
- function saveSpecFileContent() {
- const file = document.getElementById("specFileSelect").value;
- const content = document.getElementById("specContentTextarea").value;
- const status = document.getElementById("specSaveStatus");
-
- status.textContent = "⏳ 正在保存到磁盘...";
- status.style.color = "var(--amber)";
-
- fetch("/api/save_spec", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ file, content })
- })
- .then(r => r.json().then(d => ({ ok: r.ok, d })))
- .then(({ ok, d }) => {
- if (ok) {
- status.textContent = "✅ 保存成功!新的 Prompt 规则已在本地生效。";
- status.style.color = "var(--mint)";
- setTimeout(() => {
- if (status.textContent.includes("保存成功")) {
- status.textContent = "";
- }
- }, 4000);
- } else {
- status.textContent = "❌ 保存失败: " + (d.error || "未知错误");
- status.style.color = "var(--rose)";
- }
- })
- .catch(err => {
- status.textContent = "❌ 网络错误: " + err;
- status.style.color = "var(--rose)";
- });
- }
- function esc(s) { return (s === undefined || s === null ? "" : String(s)).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); }
- const ACTIONS = [{ "name": "检索", "l1": "获取", "l2": "搜索" }, { "name": "下载", "l1": "获取", "l2": "搜索" }, { "name": "调取", "l1": "获取", "l2": "查询" }, { "name": "上传", "l1": "获取", "l2": "录入" }, { "name": "拍摄", "l1": "获取", "l2": "录入" }, { "name": "录音", "l1": "获取", "l2": "录入" }, { "name": "键入", "l1": "获取", "l2": "录入" }, { "name": "选取", "l1": "获取", "l2": "引用" }, { "name": "裁切", "l1": "提取", "l2": "物理提取" }, { "name": "抠取", "l1": "提取", "l2": "物理提取" }, { "name": "抽帧", "l1": "提取", "l2": "物理提取" }, { "name": "识别", "l1": "提取", "l2": "化学提取" }, { "name": "反推", "l1": "提取", "l2": "化学提取" }, { "name": "解构", "l1": "提取", "l2": "化学提取" }, { "name": "元素生成", "l1": "生成", "l2": "元素生成" }, { "name": "数组生成", "l1": "生成", "l2": "关系生成" }, { "name": "结构生成", "l1": "生成", "l2": "关系生成" }, { "name": "添加", "l1": "修改", "l2": "增" }, { "name": "叠加", "l1": "修改", "l2": "增" }, { "name": "抹除", "l1": "修改", "l2": "删" }, { "name": "剪除", "l1": "修改", "l2": "删" }, { "name": "重述", "l1": "修改", "l2": "变" }, { "name": "风格化", "l1": "修改", "l2": "变" }, { "name": "转换", "l1": "修改", "l2": "变" }, { "name": "替换", "l1": "修改", "l2": "变" }, { "name": "调整", "l1": "修改", "l2": "变" }, { "name": "增强", "l1": "修改", "l2": "变" }];
- const TYPES = [{ "name": "提示词", "l1": "程序控制类型", "l2": "指令" }, { "name": "负向提示词", "l1": "程序控制类型", "l2": "指令" }, { "name": "描述", "l1": "程序控制类型", "l2": "指令" }, { "name": "生成参数", "l1": "程序控制类型", "l2": "参数" }, { "name": "规格参数", "l1": "程序控制类型", "l2": "参数" }, { "name": "模型权重", "l1": "程序控制类型", "l2": "参数" }, { "name": "评分", "l1": "程序控制类型", "l2": "评估" }, { "name": "评语", "l1": "程序控制类型", "l2": "评估" }, { "name": "工作流", "l1": "程序控制类型", "l2": "流程" }, { "name": "批处理", "l1": "程序控制类型", "l2": "流程" }, { "name": "数字人", "l1": "数据复用类型", "l2": "原子" }, { "name": "版式", "l1": "数据复用类型", "l2": "原子" }, { "name": "模板", "l1": "数据复用类型", "l2": "序列" }, { "name": "参考图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "参考视频", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "参考音频", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "对标内容", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "分镜图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "转场", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "蒙版", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "控制图", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "运动轨迹", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "滤镜", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "构图布局", "l1": "内容类型", "l2": "素材/化学变化" }, { "name": "截图", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "视频片段", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "转场片段", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "关键帧", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "音效", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "特效", "l1": "内容类型", "l2": "素材/物理变化" }, { "name": "大纲", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "分镜脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "剪辑脚本", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "配音文案", "l1": "内容类型", "l2": "半成品/序列" }, { "name": "底图", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "样图", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "分镜视频", "l1": "内容类型", "l2": "半成品/原子" }, { "name": "图层组合", "l1": "内容类型", "l2": "半成品/组合" }, { "name": "拼图", "l1": "内容类型", "l2": "半成品/组合" }, { "name": "歌词", "l1": "内容类型", "l2": "准成品" }, { "name": "配音", "l1": "内容类型", "l2": "准成品" }, { "name": "BGM", "l1": "内容类型", "l2": "准成品" }, { "name": "字幕", "l1": "内容类型", "l2": "准成品" }, { "name": "标题", "l1": "内容类型", "l2": "准成品" }, { "name": "正文", "l1": "内容类型", "l2": "准成品" }, { "name": "成品图", "l1": "内容类型", "l2": "成品" }, { "name": "视频成品", "l1": "内容类型", "l2": "成品" }, { "name": "合成图", "l1": "内容类型", "l2": "成品" }, { "name": "知识库", "l1": "知识类型", "l2": "知识库" }];
- const POOLS = { "action_leaves": { "检索": ["找", "搜", "哪里找", "在哪找", "搜索"], "下载": ["下载", "怎么下载", "获取", "导入"], "调取": ["复用", "调用", "套用", "加载"], "上传": ["上传", "导入", "怎么导入", "用自己的"], "拍摄": ["拍", "拍摄", "录制", "录屏", "怎么拍"], "录音": ["录音", "录", "怎么录", "录制"], "键入": ["写", "输入", "怎么写", "编写"], "选取": ["挑选", "怎么选", "筛选", "选"], "裁切": ["裁剪", "截取", "怎么裁", "切片"], "抠取": ["抠图", "抠", "分割", "怎么抠"], "抽帧": ["抽帧", "提取帧", "怎么抽帧", "导出帧"], "识别": ["识别", "检测", "OCR", "提取文字", "转录"], "反推": ["反推", "分析", "推断", "怎么反推"], "解构": ["拆解", "解构", "分析结构"], "元素生成": ["生成", "制作", "怎么做", "做", "创作"], "数组生成": ["批量生成", "生成多张", "怎么批量", "批量做"], "结构生成": ["生成", "搭建", "怎么生成", "构建"], "添加": ["添加", "加", "怎么加", "加上"], "叠加": ["叠加", "合成", "叠", "加"], "抹除": ["去除", "去掉", "怎么去掉", "抹除", "消除"], "剪除": ["剪掉", "删掉", "怎么剪", "截短"], "重述": ["改写", "润色", "怎么改", "重写"], "风格化": ["风格化", "转风格", "风格迁移", "怎么转风格"], "转换": ["转换", "转", "怎么转", "转格式"], "替换": ["替换", "换", "怎么换", "替换成"], "调整": ["调整", "调", "怎么调", "优化"], "增强": ["增强", "提升画质", "怎么增强", "修复", "超分"] }, "types": { "提示词": ["提示词", "prompt", "咒语"], "负向提示词": ["负向提示词", "反向提示词", "negative prompt"], "描述": ["描述", "描述词", "提示描述"], "生成参数": ["参数", "出图参数", "生成参数", "seed"], "规格参数": ["尺寸", "分辨率", "画幅", "规格"], "模型权重": ["模型", "LoRA", "底模", "checkpoint"], "评分": ["评分", "打分", "评级"], "评语": ["评语", "点评", "反馈", "修改意见"], "工作流": ["工作流", "workflow", "流程图", "节点"], "批处理": ["批处理", "批量", "batch"], "数字人": ["数字人", "虚拟人", "数字分身", "AI分身"], "版式": ["版式", "排版", "版面", "layout"], "模板": ["模板", "template", "套版"], "参考图": ["参考图", "参考", "垫图", "ref"], "参考视频": ["参考视频", "参考片", "参考素材"], "参考音频": ["参考音频", "参考音色", "音色样本"], "对标内容": ["对标", "爆款参考", "竞品", "标杆案例"], "分镜图": ["分镜图", "分镜", "故事板", "storyboard"], "转场": ["转场", "转场效果", "transition"], "蒙版": ["蒙版", "抠像", "遮罩", "mask"], "控制图": ["控制图", "ControlNet", "结构图", "线稿"], "运动轨迹": ["运镜", "运动轨迹", "运动笔刷", "motion brush"], "滤镜": ["滤镜", "filter", "调色", "LUT"], "构图布局": ["构图", "布局", "构图布局"], "截图": ["截图", "屏幕截图", "截屏"], "视频片段": ["视频片段", "片段", "素材", "clip"], "转场片段": ["转场片段", "转场素材"], "关键帧": ["关键帧", "帧", "视频帧"], "音效": ["音效", "声音", "SFX", "音效素材"], "特效": ["特效", "视频特效", "VFX", "effect"], "大纲": ["大纲", "内容大纲", "提纲"], "脚本": ["脚本", "文案脚本", "剧本"], "分镜脚本": ["分镜脚本", "分镜表", "分镜"], "剪辑脚本": ["剪辑脚本", "剪辑表", "卡点表", "时间轴"], "配音文案": ["配音文案", "口播文案", "旁白文案"], "底图": ["底图", "背景图", "打底图"], "样图": ["样图", "效果图", "候选图", "draft"], "分镜视频": ["分镜视频", "分镜片段"], "图层组合": ["图层组合", "图层", "图层合成"], "拼图": ["拼图", "九宫格", "长图", "对比图"], "歌词": ["歌词", "词", "lyrics"], "配音": ["配音", "旁白", "解说", "人声"], "BGM": ["BGM", "背景音乐", "配乐"], "字幕": ["字幕", "视频字幕", "字幕条"], "标题": ["标题", "文案标题", "爆款标题"], "正文": ["正文", "文案", "内容正文"], "成品图": ["成品图", "出图", "图", "海报"], "视频成品": ["视频成品", "成片", "视频"], "合成图": ["合成图", "拼合图", "融合图", "合成"], "知识库": ["知识库", "资料库", "knowledge base"] }, "knowledge": { "工序": { "单步": ["教程", "流程", "步骤", "怎么做", "方法", "教学"], "全程": ["完整流程", "全流程", "pipeline", "SOP"] }, "能力": { "标记": ["一键", "自动", "直出", "秒出"], "达成": ["同款", "复刻", "效果"], "载体": ["提示词", "参数", "预设", "公式"] }, "工具": { "发现": ["用什么软件", "用什么工具", "工具推荐", "哪个好用", "有哪些工具"] } }, "tool_type": { "AI 模型": ["AI"], "桌面 APP": ["软件", "电脑端"], "云端 Web": ["在线", "网页版"], "API·CLI": ["代码", "命令行"], "插件扩展": ["插件"] } };
- const TOOL_TYPES = ["AI 模型", "桌面 APP", "云端 Web", "API·CLI", "插件扩展"];
- const GMODEL = "gemini-3.1-flash-lite";
- const AL = POOLS.action_leaves, TP = POOLS.types, KN = POOLS.knowledge, TQ = POOLS.tool_type;
- function aPool(a) { return AL[a] || [a]; }
- function tPool(t) { return TP[t] || [t]; }
- function pick(arr, i) { return arr[Math.min(i, arr.length - 1)]; }
- function toolPrefix(lens) { return (st.tools.length && lens !== '工具') ? st.tools.map(t => TQ[t][0]).join('/') + ' ' : ''; }
- function genForms(leaf, ty, lens, tq) {
- tq = tq || ''; const aP = aPool(leaf), tP = tPool(ty);
- const aNat = aP[0], tNat = tP[0], aSyn = pick(aP, 1), tSyn = pick(tP, 1), K = KN[lens];
- if (lens === '工序') {
- const s = K['单步'];
- return [['①原词', `${tq}${leaf} ${ty} 流程`], ['②句子', `${tq}怎么${aNat}${tNat}`], ['③同义', `${tq}${aSyn} ${tSyn} ${pick(s, 2)}`]];
- }
- if (lens === '能力') {
- const mk = K['标记'];
- return [['①原词', `${tq}${leaf} ${ty} 技巧`], ['②句子', `${tq}有没有能${mk[0]}${aNat}${tNat}的功能`], ['③同义', `${tq}${pick(mk, 1)} ${aSyn}${tSyn}`]];
- }
- const fd = K['发现'];
- return [['①原词', `${leaf} ${ty} 工具`], ['②句子', `${aNat}${tNat}用什么软件好`], ['③同义', `${aSyn} ${tSyn} ${pick(fd, 2)}`]];
- }
- const FIDX = { A: 0, B: 1, C: 2 };
- function genQ(leaf, ty) { return genForms(leaf, ty, st.lens, toolPrefix(st.lens))[FIDX[st.form]][1]; }
- // 兼容老版 1-5 维度的英文和中文对应标签
- const commonLabels = { relevance: "相关性", result_quality: "成品质量", credibility: "可信度", novelty_coverage: "新增覆盖", concrete_use_case: "具体用例", completeness: "流程完整", step_structure: "步骤结构", step_reproducibility: "可复现性", capability_definition: "能力定义", implementation_depth: "实现深度", boundary_failure_eval: "边界/踩坑", generality: "通用性", capability_coverage: "工具覆盖", effective_comparison: "有效对比", param_specificity: "参数具体", worked_example: "示例完整", version_limits: "限制说明" };
- const filterLabels = { production_relevance: "制作相关性", recency_hard: "发布时效", overall: "综合均分" };
- const filterMax = { production_relevance: 3, recency_hard: 3, overall: 5 };
-
- // 新版 0-10 分数维度的中文标签
- const newLabels = {
- relevance_production: "和内容制作知识相关",
- relevance_query: "和 query 相关",
- recency: "时效性",
- popularity: "热度性",
- feedback: "评论反馈",
- realism: "真实感 (非AI)",
- expressiveness: "表现力",
- procedure_completeness: "流程完整性",
- procedure_input: "输入完整性",
- procedure_implementation: "实现完整性",
- procedure_output: "输出完整性",
- procedure_generality: "泛化性",
- step_input: "输入完整性",
- step_implementation: "实现完整性",
- step_output: "输出完整性",
- step_generality: "泛化性",
- tool_boundary: "能力边界覆盖",
- tool_comparison: "有效比较",
- tool_specificity: "参数/接口具体性",
- tool_example: "实操示例",
- tool_limits: "版本&限制"
- };
- const scoreGroupsOld = [
- { id: "filter", title: "过滤指标", short: "过滤", hint: "独立于通用维度·过滤逻辑待定", topLevelKeys: ["production_relevance", "recency_hard", "overall"] },
- { id: "common", title: "通用维度", short: "通用", keys: ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"] },
- { id: "procedure", title: "工序维度", short: "工序", keys: ["completeness", "step_structure", "step_reproducibility"] },
- { id: "step", title: "步骤维度", short: "步骤", keys: ["capability_definition", "implementation_depth", "boundary_failure_eval", "generality"] },
- { id: "tool", title: "工具维度", short: "工具", keys: ["capability_coverage", "effective_comparison", "param_specificity", "worked_example", "version_limits"] }
- ];
- const scoreGroupsNew = [
- { id: "relevance", title: "相关性", short: "相关", keys: ["relevance_production", "relevance_query"] },
- { id: "fixed", title: "固定维度", short: "固定", keys: ["recency", "popularity", "feedback"] },
- { id: "usecase", title: "用例维度", short: "用例", keys: ["realism", "expressiveness"] },
- { id: "dynamic", title: "动态维度", short: "动态", keys: [] }
- ];
- function makeRow(label, scoreKey, it) {
- const rawV = it.scores[scoreKey];
- const v = (rawV !== undefined && rawV !== null) ? parseFloat(rawV) : NaN;
- const hasScore = !isNaN(v);
- const valStr = hasScore ? (Number.isInteger(v) ? v : v.toFixed(1)) : '-';
- const barV = hasScore ? v : 0;
- const reason = it.score_reasons ? it.score_reasons[scoreKey] : '';
- const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(label)}', '${esc(scoreKey)}')" title="点击定格查看评判理由" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7; font-size: 13px; font-weight: normal; user-select: none;">ⓘ</span>` : '';
- return `
- <div class="sc-row ${!hasScore ? 'missing' : ''}">
- <span class="label">${esc(label)}</span>
- <div class="bar-wrap">
- <div class="bar">
- <div class="bar-fill" style="--v: ${barV}"></div>
- </div>
- <span class="value">${valStr}</span>
- ${infoIcon}
- </div>
- </div>
- `;
- }
- function getQualityAverage(it) {
- if (!it.scores) return null;
- const keys = ["recency", "popularity", "feedback", "realism", "expressiveness"];
- if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
- keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
- }
- if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
- keys.push("step_input", "step_implementation", "step_output", "step_generality");
- }
- if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
- keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
- }
- const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
- return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
- }
- function renderNewScores(it) {
- // 1. Relevance Card
- const relAvg = groupAverage(it, scoreGroupsNew[0], true);
- const relAvgStr = relAvg !== null ? relAvg.toFixed(1) : 'N/A';
-
- let relevanceHtml = `
- <div class="sc-card">
- <div class="sc-card-head">
- <div class="title"><span class="badge">01</span>相关性</div>
- <div class="avg-score">avg <strong>${relAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
- </div>
- <div class="sc-card-body">
- ${makeRow("和内容制作知识相关", "relevance_production", it)}
- ${makeRow("和 query 相关", "relevance_query", it)}
- </div>
- </div>
- `;
- // 2. Quality Card
- const qualAvg = getQualityAverage(it);
- const qualAvgStr = qualAvg !== null ? qualAvg.toFixed(1) : 'N/A';
- let qualityHtml = `
- <div class="sc-card">
- <div class="sc-card-head">
- <div class="title"><span class="badge">02</span>质量</div>
- <div class="avg-score">avg <strong>${qualAvgStr}</strong><span style="font-size: 13px; color: #9ca3af; font-weight: 500;">/10</span></div>
- </div>
- <div class="sc-card-body">
- <div class="sc-sub-header">固定维度</div>
- ${makeRow("时效性", "recency", it)}
- ${makeRow("热度性", "popularity", it)}
- ${makeRow("评论反馈", "feedback", it)}
-
- <div class="sc-sub-header">用例</div>
- ${makeRow("真实感 (非AI)", "realism", it)}
- ${makeRow("表现力", "expressiveness", it)}
- `;
- // Dynamic Dimensions
- let dynamicHtml = '';
- if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
- dynamicHtml += `
- <div class="sc-sub-header">工序</div>
- ${makeRow("流程完整性", "procedure_completeness", it)}
- ${makeRow("输入完整性", "procedure_input", it)}
- ${makeRow("实现完整性", "procedure_implementation", it)}
- ${makeRow("输出完整性", "procedure_output", it)}
- ${makeRow("泛化性", "procedure_generality", it)}
- `;
- }
- if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
- dynamicHtml += `
- <div class="sc-sub-header">能力</div>
- ${makeRow("输入完整性", "step_input", it)}
- ${makeRow("实现完整性", "step_implementation", it)}
- ${makeRow("输出完整性", "step_output", it)}
- ${makeRow("泛化性", "step_generality", it)}
- `;
- }
- if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
- dynamicHtml += `
- <div class="sc-sub-header">工具</div>
- ${makeRow("能力边界覆盖", "tool_boundary", it)}
- ${makeRow("有效比较", "tool_comparison", it)}
- ${makeRow("参数/接口具体性", "tool_specificity", it)}
- ${makeRow("实操示例", "tool_example", it)}
- ${makeRow("版本&限制", "tool_limits", it)}
- `;
- }
- qualityHtml += dynamicHtml + `
- </div>
- </div>
- `;
- return relevanceHtml + qualityHtml;
- }
- function isItemDiscarded(it) {
- if (it.anomaly) return false;
-
- const input = document.getElementById("relThreshold");
- const userThreshold = input ? parseFloat(input.value) : NaN;
-
- const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
-
- let isDiscard = false;
-
- // 1. Relevance check
- const relVal = it.production_relevance !== null && it.production_relevance !== undefined ? parseFloat(it.production_relevance) : null;
- if (relVal !== null && !isNaN(relVal)) {
- const activeThreshold = !isNaN(userThreshold) ? userThreshold : (isNewSchema ? 4.0 : 2.0);
- if (relVal < activeThreshold) {
- isDiscard = true;
- }
- }
-
- // 2. Recency check
- if (it.recency_hard !== null && it.recency_hard !== undefined && it.recency_hard < 2) {
- isDiscard = true;
- }
-
- // 3. Overall average check
- if (it.overall !== null && it.overall !== undefined) {
- const threshold_ov = isNewSchema ? 6.0 : 3.0;
- if (it.overall < threshold_ov) {
- isDiscard = true;
- }
- }
-
- return isDiscard;
- }
- function updateThresholdLimits() {
- const f = curForm();
- const input = document.getElementById("relThreshold");
- if (!f || !f.results || f.results.length === 0 || !input) return;
- const isNew = f.results.some(r => r.scores && (r.scores.relevance_production !== undefined || r.scores.relevance_query !== undefined));
- const schemaKey = isNew ? "new" : "old";
- if (input.dataset.schema !== schemaKey) {
- input.dataset.schema = schemaKey;
- input.max = isNew ? "10" : "5";
- input.value = isNew ? "4.0" : "2.0";
- input.step = isNew ? "0.5" : "1";
- }
- }
- const PLATC = { xhs: "小红书", gzh: "公众号", zhihu: "知乎", x: "X", bili: "B站", douyin: "抖音", sph: "视频号", youtube: "YouTube", github: "GitHub", toutiao: "头条", weibo: "微博" };
- const FN = { A: "原词", B: "句子", C: "同义" };
- 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";
- function curForm() { return st.qi === -1 ? null : (DATA.queries[st.qi] ? DATA.queries[st.qi].forms[st.fi] : null); }
-
- function groupAverage(it, g, isNewSchema) {
- if (!it.scores) return null;
- let keys = g.keys;
- if (isNewSchema && g.id === "dynamic") {
- keys = [];
- if (it.knowledge_type && (it.knowledge_type.includes("procedure") || it.knowledge_type.includes("工序"))) {
- keys.push("procedure_completeness", "procedure_input", "procedure_implementation", "procedure_output", "procedure_generality");
- }
- if (it.knowledge_type && (it.knowledge_type.includes("step") || it.knowledge_type.includes("能力") || it.knowledge_type.includes("步骤"))) {
- keys.push("step_input", "step_implementation", "step_output", "step_generality");
- }
- if (it.knowledge_type && (it.knowledge_type.includes("tool") || it.knowledge_type.includes("工具"))) {
- keys.push("tool_boundary", "tool_comparison", "tool_specificity", "tool_example", "tool_limits");
- }
- }
- const vs = keys.map(k => parseFloat(it.scores[k])).filter(v => !isNaN(v));
- return vs.length ? vs.reduce((a, b) => a + b, 0) / vs.length : null;
- }
-
- function fmt(v) { return v === null ? "N/A" : v.toFixed(1); }
-
- function groupSnapshot(it) {
- const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
- if (isNewSchema) {
- const relAvg = groupAverage(it, scoreGroupsNew[0], true);
- const qualAvg = getQualityAverage(it);
- return `
- <div class="group-pill"><span>相关</span><strong>${fmt(relAvg)}</strong></div>
- <div class="group-pill"><span>质量</span><strong>${fmt(qualAvg)}</strong></div>
- `;
- }
- const groups = scoreGroupsOld;
- return groups.map(g => {
- if (!isNewSchema && g.id === 'filter') {
- const pr = it.production_relevance, rh = it.recency_hard, ov = it.overall;
- const f = (v, max) => (v == null || !Number.isFinite(v)) ? '-' : (max === 5 ? (typeof v === 'number' ? v.toFixed(1) : v) : v);
- return `<div class="group-pill" title="${g.hint || ''}"><span>${g.short}</span><strong style="font-size:12px;">${f(pr, 3)}·${f(rh, 3)}·${f(ov, 5)}</strong></div>`;
- }
- const a = groupAverage(it, g, isNewSchema);
- return `<div class="group-pill"><span>${g.short}</span><strong>${fmt(a)}</strong></div>`;
- }).join("");
- }
- // filter 组:数据从 it 顶层、量程按 filterMax 归一化到 5 量程(meter 的 --v*20% 公式不动)
- function renderScoreGroup(it, g) {
- const isFilter = g.id === 'filter';
- const keys = g.topLevelKeys || g.keys;
- const src = isFilter ? it : it.scores;
- if (!src) return '';
- const labels = isFilter ? filterLabels : commonLabels;
- const rows = keys.map(k => {
- const v = src[k];
- const m = !Number.isFinite(v);
- const max = isFilter ? (filterMax[k] || 5) : 5;
- const meterV = m ? 0 : (v * 5 / max);
- const suffix = isFilter && max !== 5 ? `/${max}` : '';
- const valStr = m ? '-' : (typeof v === 'number' ? (Number.isInteger(v) ? v : v.toFixed(1)) : v);
- const reason = (it.score_reasons) ? it.score_reasons[k] : '';
- const infoIcon = reason ? `<span class="info-icon" onclick="pinScoreReason(this, '${esc(labels[k] || k)}', '${esc(k)}')" title="点击定格查看评判理由" style="margin-left: 5px; cursor: pointer; color: var(--muted); opacity: 0.7; font-size: 11px; font-weight: normal; user-select: none;">ⓘ</span>` : '';
- return `<div class="score-row ${m ? 'missing' : ''}"><span>${labels[k] || k}</span><div class="meter" style="--v:${meterV}"><span></span></div><strong style="display: inline-flex; align-items: center;">${valStr}${suffix}${infoIcon}</strong></div>`;
- }).join("");
- const headRight = isFilter ? (g.hint ? `<small>${g.hint}</small>` : '') : `<small>均分 ${fmt(groupAverage(it, g))}</small>`;
- return `<section class="score-group"><div class="score-group-head"><span>${g.title}</span>${headRight}</div><div class="score-group-body">${rows}</div></section>`;
- }
- function curDims() {
- if (st.qi === -1) { if (openCell) { const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti; return { type: TYPES[ti].name, action: ACTIONS[ai].name }; } return {}; }
- const q = DATA.queries[st.qi]; return (q && q.dims) ? q.dims : {};
- }
- function spans(arr, key) { const o = []; let cur = null, s = 0; arr.forEach((x, i) => { if (x[key] !== cur) { if (cur !== null) o.push([cur, s, i - s]); cur = x[key]; s = i; } }); o.push([cur, s, arr.length - s]); return o; }
- const l1sp = spans(ACTIONS, 'l1'), l2sp = spans(ACTIONS, 'l2');
- const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
- function detectLens(q) {
- const text = (q.original_q || (q.forms && q.forms[0] && q.forms[0].query) || "").toLowerCase();
- if (/流程|步骤|教程|方法|教学|SOP|pipeline|工序/.test(text)) return '工序';
- if (/一键|自动|直出|秒出|同款|复刻|效果|技巧|能力/.test(text)) return '能力';
- if (/用什么|软件|工具|推荐|哪个好用|有哪些/.test(text)) return '工具';
- return '工序';
- }
- function findQuery(aName, tName, lens, tool) {
- const matches = DATA.queries.filter(q => {
- if (!q.dims || q.dims.action !== aName || q.dims.type !== tName) return false;
- const qLens = detectLens(q);
- if (qLens !== lens) return false;
-
- const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
- if (tool) {
- if (!hasToolConstraint || q.dims.constraint.value !== tool) return false;
- }
- return true;
- });
- if (matches.length > 0) {
- matches.sort((x, y) => {
- if (!tool) {
- const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
- const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
- if (hasX !== hasY) return hasX ? 1 : -1;
- }
- return (y.hits || 0) - (x.hits || 0);
- });
- return matches[0];
- }
- return null;
- }
- function selectQueryByActiveCellAndControls(aName, tName) {
- const activeTool = st.tools[0] || null;
- let match = findQuery(aName, tName, st.lens, activeTool);
-
- const isProcedureView = st.matrixView === 'procedures';
- const isHitsView = st.matrixView === 'hits';
- const hasContent = q => {
- if (isProcedureView) return getFormProcedureCount(q) > 0;
- if (isHitsView) return getFormReportCount(q) > 0;
- return q.hits > 0;
- };
- if (match) {
- st.qi = DATA.queries.indexOf(match);
- const fi = match.forms.findIndex(f => f.form === st.form);
- st.fi = fi >= 0 ? fi : 0;
- st.selectedAction = aName;
- st.selectedType = tName;
- } else {
- // Fallback: search for any query matching active lens & tool constraint with hits
- const matches = DATA.queries.filter(q => {
- if (!q.dims || !q.dims.action || !q.dims.type) return false;
- const qLens = detectLens(q);
- if (qLens !== st.lens) return false;
-
- const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
- if (activeTool) {
- if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
- } else {
- if (hasToolConstraint) return false;
- }
- return hasContent(q);
- });
- if (matches.length > 0) {
- matches.sort((x, y) => {
- if (!activeTool) {
- const hasX = x.dims.constraint && x.dims.constraint.kind === "工具类型";
- const hasY = y.dims.constraint && y.dims.constraint.kind === "工具类型";
- if (hasX !== hasY) return hasX ? 1 : -1;
- }
- const countX = isProcedureView ? getFormProcedureCount(x) : (isHitsView ? getFormReportCount(x) : x.hits);
- const countY = isProcedureView ? getFormProcedureCount(y) : (isHitsView ? getFormReportCount(y) : y.hits);
- return countY - countX;
- });
- const anyMatch = matches[0];
- st.qi = DATA.queries.indexOf(anyMatch);
- const fi = anyMatch.forms.findIndex(f => f.form === st.form);
- st.fi = fi >= 0 ? fi : 0;
- st.selectedAction = anyMatch.dims.action;
- st.selectedType = anyMatch.dims.type;
- } else {
- st.qi = -1;
- st.fi = 0;
- }
- }
- }
- function getFormReportCount(q) {
- const f = q.forms && q.forms.find(x => x.form === st.form);
- if (!f || !f.results) return 0;
- return f.results.filter(r => !r.anomaly && !isItemDiscarded(r)).length;
- }
- function getFormProcedureCount(q) {
- const f = q.forms && q.forms.find(x => x.form === st.form);
- if (!f || !f.results) return 0;
- return f.results.filter(r => !r.anomaly && !isItemDiscarded(r) && r.procedure_html).length;
- }
- function getFilteredHitsMap() {
- const cm = {};
- const activeTool = st.tools[0] || null;
- const isProcedureView = st.matrixView === 'procedures';
- DATA.queries.forEach((q, i) => {
- const d = q.dims;
- if (!d || !d.type || !d.action) return;
- // Filter by lens
- if (detectLens(q) !== st.lens) return;
- // Filter by active tool constraint
- const hasToolConstraint = d.constraint && d.constraint.kind === "工具类型";
- if (activeTool) {
- if (!hasToolConstraint || d.constraint.value !== activeTool) return;
- } else {
- if (hasToolConstraint) return;
- }
- const k = d.type + '|' + d.action;
- const h = isProcedureView ? getFormProcedureCount(q) : getFormReportCount(q);
- if (!cm[k] || h > cm[k].hits) {
- cm[k] = { i, hits: h };
- }
- });
- return cm;
- }
- function renderMatrix() {
- const viewMode = st.matrixView || 'full';
- const showFull = viewMode === 'full';
- const cm = getFilteredHitsMap();
- const activeActions = showFull ? ACTIONS : ACTIONS.filter(a => TYPES.some(t => {
- const c = cm[t.name + '|' + a.name];
- return c && c.hits > 0;
- }));
- const activeTypes = showFull ? TYPES : TYPES.filter(t => ACTIONS.some(a => {
- const c = cm[t.name + '|' + a.name];
- return c && c.hits > 0;
- }));
- const displayActions = activeActions.length ? activeActions : ACTIONS;
- const displayTypes = activeTypes.length ? activeTypes : TYPES;
- const l1sp = spans(displayActions, 'l1'), l2sp = spans(displayActions, 'l2');
- const l1Start = new Set(l1sp.map(s => s[1])), l2Start = new Set(l2sp.map(s => s[1]));
- let h = '<thead><tr class="l1"><th class="corner" rowspan="3">类型 \ 动作</th>' + l1sp.map(([v, s, c]) => `<th colspan="${c}" class="l1div">${v}</th>`).join('') + '</tr>';
- h += '<tr class="l2">' + l2sp.map(([v, s, c]) => `<th colspan="${c}" class="${l1Start.has(s) ? 'l1div' : 'l2div'}">${v}</th>`).join('') + '</tr>';
- h += '<tr class="leaf">' + displayActions.map((a, i) => `<th data-ai="${ACTIONS.indexOf(a)}" class="${l1Start.has(i) ? 'l1div' : (l2Start.has(i) ? 'l2div' : '')}">${a.name}</th>`).join('') + '</tr></thead><tbody>';
- const typeCategories = ['程序控制类型', '数据复用类型', '内容类型', '知识类型'];
- typeCategories.forEach(l1 => {
- const catTypes = displayTypes.filter(t => t.l1 === l1);
- if (catTypes.length === 0) return;
- h += `<tr class="l1row"><td colspan="${displayActions.length + 1}">${l1}</td></tr>`;
- catTypes.forEach((t) => {
- h += `<tr data-type="${t.name}"><th class="rh" data-ti="${TYPES.indexOf(t)}">${t.name}</th>` + displayActions.map((a) => {
- const ai = ACTIONS.indexOf(a);
- const ti = TYPES.indexOf(t);
- const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
- const s = cell.tier !== undefined ? cell.tier : cell.s;
- const cls = (s === null || s === undefined) ? 'tNA' : ('t' + s);
- const isSel = (st.selectedAction === a.name && st.selectedType === t.name) ? ' sel' : '';
- return `<td class="cell ${cls}${isSel}" data-ai="${ai}" data-ti="${ti}"></td>`;
- }).join('') + '</tr>';
- });
- });
- document.getElementById('comboMx').innerHTML = h + '</tbody>';
- refresh();
- applyCrosshair();
- }
- // 点中 cell 时浅高亮整行整列(含 row/col 表头),方便定位
- function applyCrosshair() {
- document.querySelectorAll('.rowsel,.colsel').forEach(el => el.classList.remove('rowsel', 'colsel'));
- const ai = st.selectedAction ? ACTIONS.findIndex(a => a.name === st.selectedAction) : -1;
- const ti = st.selectedType ? TYPES.findIndex(t => t.name === st.selectedType) : -1;
- if (ai < 0 && ti < 0) return;
- document.querySelectorAll(`#comboMx [data-ai="${ai}"]`).forEach(el => el.classList.add('colsel'));
- document.querySelectorAll(`#comboMx [data-ti="${ti}"]`).forEach(el => el.classList.add('rowsel'));
- }
- function refresh() {
- const cm = getFilteredHitsMap();
- document.querySelectorAll('tr[data-type]').forEach(tr => {
- const ty = tr.dataset.type;
- tr.querySelectorAll('td.cell').forEach(td => {
- const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai];
- const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
- const s = cell.tier !== undefined ? cell.tier : cell.s;
- const c = cm[ty + '|' + a.name];
- const hits = c ? c.hits : 0;
- td.textContent = genQ(a.name, ty);
- if (hits > 0) {
- const badge = document.createElement('span');
- badge.className = 'hit-badge';
- badge.textContent = hits;
- td.appendChild(badge);
- }
- td.classList.toggle('hide', (s == null ? 0 : s) < (+st.tier));
- const hitsLabel = st.matrixView === 'procedures' ? '有工序' : '命中';
- td.title = `${ty} × ${a.name}\nGemini评分: ${(s === null || s === undefined) ? '未判' : ['无效', '低', '中', '高'][s]}\n当前 form(${st.form}) ${hitsLabel}: ${hits} 篇`;
- });
- });
- }
- const pop = document.getElementById('pop'); let openCell = null;
- function showPop(td, x, y) {
- openCell = td;
- const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
- const cell = (DATA.matrix && DATA.matrix[ai]) ? (DATA.matrix[ai][ti] || {}) : {};
- const s = cell.tier !== undefined ? cell.tier : cell.s;
- const tn = (s == null) ? '未判' : ['无效', '低', '中', '高'][s], tb = (s == null) ? 'NA' : s;
- const gen = genForms(a.name, t.name, st.lens, toolPrefix(st.lens));
- // Find matching query in database for this cell
- const activeTool = st.tools[0] || null;
- const matches = DATA.queries.filter(q => {
- if (!q.dims || q.dims.action !== a.name || q.dims.type !== t.name) return false;
- const qLens = detectLens(q);
- if (qLens !== st.lens) return false;
- const hasToolConstraint = q.dims.constraint && q.dims.constraint.kind === "工具类型";
- if (activeTool) {
- if (!hasToolConstraint || q.dims.constraint.value !== activeTool) return false;
- } else {
- if (hasToolConstraint) return false;
- }
- return true;
- });
- let dbQuery = null;
- if (matches.length > 0) {
- matches.sort((x, y) => (y.hits || 0) - (x.hits || 0));
- dbQuery = matches[0];
- }
- const dbFormMap = {};
- if (dbQuery) {
- dbQuery.forms.forEach(f => {
- dbFormMap[f.form] = f.query;
- });
- }
- const formKeys = ['A', 'B', 'C'];
- let html = `<span class="close">×</span>
- <div class="pt">
- <span class="path a">${a.l1}›${a.l2}›${a.name}</span>
- <span class="path t">${t.l1}›${t.name}</span>
- <span class="path" style="background: var(--soft-amber); color: var(--amber); font-family: monospace; font-weight: bold; border: 1px solid rgba(184, 121, 24, 0.2);">${dbQuery ? dbQuery.key : '未匹配'}</span>
- <span class="tier tb${tb}">gemini·${tn}</span>
- </div>
- <div class="reason${s === 0 ? ' inv' : ''}">${esc(cell.r || '(模型未给该格评分)')}</div>
- <div class="lbl">系统生成 · 知识类型=${st.lens}${st.tools.length ? ' · 工具类型=' + st.tools.join('/') : ''}</div>
- <ul>` + gen.map(([fName, genQStr], idx) => {
- const formKey = formKeys[idx];
- const actualQ = dbFormMap[formKey];
- const isCurrent = formKey === st.form;
- let content = `<span class="fm">${fName}</span>${esc(genQStr)}`;
- if (actualQ) {
- if (actualQ !== genQStr) {
- content += `<div style="font-size: 13px; margin-top: 6px; color: #047857; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #ecfdf5; border: 1px dashed #34d399; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(4, 120, 87, 0.04);">
- <span style="background: #10b981; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
- <span style="word-break: break-all; font-family: inherit;">${esc(actualQ)}</span>
- </div>`;
- } else {
- content += `<div style="font-size: 13px; margin-top: 6px; color: #374151; font-weight: 600; font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; line-height: 1.45; background: #f9fafb; border: 1px dashed #d1d5db; padding: 6px 10px; border-radius: 6px; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 4px; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);">
- <span style="background: #9ca3af; padding: 2px 6px; border-radius: 4px; font-size: 11px; color: #fff; font-weight: bold; font-family: -apple-system, sans-serif; white-space: nowrap;">实际搜索</span>
- <span style="font-style: italic; font-family: inherit; color: #6b7280;">(同上)</span>
- </div>`;
- }
- }
- return `<li class="gen${isCurrent ? ' cur' : ''}" style="display: flex; flex-direction: column; align-items: flex-start; gap: 2px; padding: 6px 8px;">${content}</li>`;
- }).join('') + '</ul>';
- pop.innerHTML = html;
- pop.style.display = 'block';
- pop.style.left = Math.max(8, Math.min(x, window.innerWidth - 466)) + 'px';
- pop.style.top = Math.max(8, Math.min(y, window.innerHeight - pop.offsetHeight - 12)) + 'px';
- pop.querySelector('.close').onclick = () => {
- pop.style.display = 'none';
- openCell = null;
- };
- }
- function computeStats() {
- const stat = { "0": 0, "1": 0, "2": 0, "3": 0, "na": 0 };
- if (DATA.matrix) {
- DATA.matrix.forEach(row => row.forEach(cell => {
- const s = cell ? (cell.tier !== undefined ? cell.tier : cell.s) : null;
- if (s === null || s === undefined) stat["na"]++;
- else if (stat[s] !== undefined) stat[s]++;
- }));
- }
- document.getElementById('s3').textContent = stat['3'];
- document.getElementById('s2').textContent = stat['2'];
- document.getElementById('s1').textContent = stat['1'];
- document.getElementById('s0').textContent = stat['0'];
- document.getElementById('sna').textContent = stat['na'];
- }
- function renderFormsChan() {
- const q = st.qi === -1 ? null : DATA.queries[st.qi];
- if (!q) {
- document.getElementById("navC").innerHTML = '';
- return;
- }
- const f = curForm();
- if (!f) {
- document.getElementById("navC").innerHTML = '';
- return;
- }
-
- const isProcedureView = st.matrixView === 'procedures';
- const resultsFilter = r => {
- if (isProcedureView) {
- return r.procedure_html && r.procedure_html !== "";
- }
- return true;
- };
-
- const filteredResults = f.results.filter(resultsFilter);
- // 渠道从实际帖子的 platformKey 推导(不依赖 f.platforms,DB 回退时它为空),按 PLATC 顺序
- const present = new Set(filteredResults.map(r => r.platformKey).filter(Boolean));
- const chans = ["all", ...Object.keys(PLATC).filter(k => present.has(k))];
- document.getElementById("navC").innerHTML = chans.map(c => {
- const n = c === "all" ? filteredResults.length : filteredResults.filter(r => r.platformKey === c).length;
- return `<span class="tab ${c === st.channel ? 'on' : ''}" data-c="${c}">${c === "all" ? "全部" : (PLATC[c] || c)} <small>${n}</small></span>`;
- }).join("");
- }
- function renderNav() { renderMatrix(); renderFormsChan(); }
- function renderHead() {
- const f = curForm();
- if (!f) return;
-
- const q = DATA.queries[st.qi];
- if (q) {
- document.getElementById("lede").innerHTML = `当前选中 Query ID: <span style="font-family: monospace; font-weight: 700; background: var(--soft-amber); color: var(--amber); padding: 2px 8px; border-radius: 4px; font-size: 15px; border: 1px solid rgba(184, 121, 24, 0.2);">${esc(q.key)}</span> · 检索语句: <span style="font-weight: 600; color: var(--ink);">${esc(q.original_q)}</span>`;
- } else {
- document.getElementById("lede").textContent = "";
- }
-
- let it = f.results;
- if (st.matrixView === 'procedures') {
- it = it.filter(r => r.procedure_html && r.procedure_html !== "");
- }
- if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
- const valid = it.filter(r => !r.anomaly);
- const rep = valid.filter(r => !isItemDiscarded(r)).length;
- const dis = valid.filter(r => isItemDiscarded(r)).length;
- const anom = it.filter(r => r.anomaly).length;
- const avg = (valid.reduce((s, r) => s + r.overall, 0) / (valid.length || 1)).toFixed(1);
- const lab = st.channel === "all" ? "该形式结果数" : (PLATC[st.channel] || st.channel) + " 结果数";
- document.getElementById("stats").innerHTML = [[it.length, lab], [rep, "建议上报"], [avg, "平均综合分 / 5"], [dis, "丢弃"], [anom, "异常"]].map(([n, l]) => `<div class="stat"><strong>${n}</strong><span>${l}</span></div>`).join("");
- }
- // POST /api/reeval —— 后台只对当前 query 的所有 form 文件复评(不重新搜索);
- // server.py 立即返回 {status:'started', pid, log},前端开启轮询自动扫描刷新。
- function reevalCurrentQuery() {
- if (st.qi === -1 || !DATA.queries[st.qi]) { alert('请先选一个 query 再重评'); return; }
- const q = DATA.queries[st.qi].key;
- if (!confirm(`重评 ${q} 的所有帖子(A/B/C 三种 form)?\n约 1-3 分钟(视帖子数),过程中页面可继续浏览。\n完成后会自动重新扫描并更新页面。`)) return;
- const btn = document.getElementById('reevalBtn');
- btn.disabled = true; btn.textContent = '♻️ 提交中…';
- fetch('/api/reeval', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ q }),
- }).then(r => r.json().then(d => ({ ok: r.ok, d }))).then(({ ok, d }) => {
- if (ok && d.status === 'started') {
- if (!DATA.active_reevals) DATA.active_reevals = {};
- DATA.active_reevals[q] = "running";
- btn.textContent = `♻️ 重评中 ${q}...`;
- startReevalPolling(q);
- } else {
- btn.disabled = false; btn.textContent = '♻️ 重评当前 query';
- alert('启动失败:' + (d.error || JSON.stringify(d)));
- }
- }).catch(e => {
- btn.disabled = false; btn.textContent = '♻️ 重评当前 query';
- alert('请求失败:' + e);
- });
- }
- function sortedItems() {
- const f = curForm();
- if (!f) return [];
- let it = f.results.slice();
- if (st.matrixView === 'procedures') {
- it = it.filter(r => r.procedure_html && r.procedure_html !== "");
- }
- if (st.channel !== "all") it = it.filter(r => r.platformKey === st.channel);
- const s = document.getElementById("sort").value;
- const decWeight = r => (r.anomaly ? 2 : (isItemDiscarded(r) ? 1 : 0));
- it.sort((a, b) => {
- const wA = decWeight(a), wB = decWeight(b);
- if (wA !== wB) return wA - wB;
- if (s === "score") return b.overall - a.overall;
- if (s === "date") return (b.date || "").localeCompare(b.date || "");
- if (s === "platform") return (a.platform || "").localeCompare(b.platform || "", "zh-Hans") || b.overall - a.overall;
- return 0;
- });
- return it;
- }
- function renderGrid() {
- VIEW = sortedItems();
- document.getElementById("grid").innerHTML = VIEW.map((it, idx) => {
- const imgs = it.images.length ? it.images.slice(0, 3).map(s => `<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" onerror="this.src='${NOIMG}'">`).join("") : `<img src="${NOIMG}">`;
-
- const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
- let bars = "";
- if (isNewSchema) {
- const relAvg = groupAverage(it, scoreGroupsNew[0], true) || 0;
- const qualAvg = getQualityAverage(it) || 0;
-
- const newBars = [
- { label: "相关性评分 (均分)", v: relAvg },
- { label: "质量评分 (均分)", v: qualAvg }
- ];
- bars = newBars.map(b => {
- const w = b.v * 10;
- return `<span title="${esc(b.label)} ${b.v.toFixed(1)}"><span class="fill" style="width:${w}%; background: #2563eb;"></span></span>`;
- }).join("");
- } else {
- const oldKeys = ["relevance", "result_quality", "credibility", "novelty_coverage", "concrete_use_case"];
- bars = oldKeys.map(k => {
- const val = it.scores[k] !== undefined ? parseFloat(it.scores[k]) : 0;
- const w = val * 20;
- return `<span title="${esc(commonLabels[k] || k)} ${val}"><span class="fill" style="width:${w}%;"></span></span>`;
- }).join("");
- }
-
- const isDiscard = isItemDiscarded(it);
- let discardReason = it.reason || '未提供过滤原因';
- if (it.production_relevance !== null && it.production_relevance !== undefined) {
- const input = document.getElementById("relThreshold");
- const threshold = input ? parseFloat(input.value) : (isNewSchema ? 4.0 : 2.0);
- const relVal = parseFloat(it.production_relevance);
- if (!isNaN(relVal) && relVal < threshold) {
- discardReason = `相关性得分 (${relVal}) 低于过滤阈值 (${threshold})`;
- }
- }
- const discardOverlayHtml = isDiscard ? `
- <div class="discard-overlay">
- <div class="discard-badge">Discarded</div>
- <div class="discard-reason">${esc(discardReason)}</div>
- </div>
- ` : '';
- return `<article class="result ${isDiscard ? 'discard' : ''}">
- ${discardOverlayHtml}
- <div class="thumbs">${imgs}</div>
- <div class="body">
- <div class="meta"><span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>${esc(it.date)} · ${esc(it.engagement)}</span></div>
- <h2>${esc(it.title)}</h2>
- <div class="excerpt">${esc(it.text)}</div>
- <div class="tags">${it.tools.slice(0, 4).map(t => `<span class="tag">${esc(t)}</span>`).join("")}</div>
- <div class="scorebar">
- <div class="overall">
- <div>
- <div class="score">${it.anomaly ? '—' : it.overall.toFixed(1)}</div>
- <small>综合分</small>
- </div>
- <div class="decision ${it.anomaly ? '' : it.decision}">${it.anomaly ? '异常' : esc(it.decision)}</div>
- </div>
- <div class="mini-bars ${isNewSchema ? 'new-schema' : ''}">${bars}</div>
- <div class="group-snapshot ${isNewSchema ? 'new-schema' : ''}">${groupSnapshot(it)}</div>
- <div class="actions">
- <button onclick="openDetail(${idx})">查看详情</button>
- <a href="${esc(it.url)}" target="_blank" rel="noreferrer">原链接</a>
- </div>
- </div>
- </div>
- </article>`;
- }).join("") || (st.qi === -1 ? '<p style="color:var(--muted);padding: 20px 0;text-align:center;grid-column: 1 / -1;">该组合暂无数据库扫描结果。</p>' : '<p style="color:var(--muted);grid-column: 1 / -1;">该渠道无结果</p>');
- }
- const ENGINE_MODELS = {
- cyber_runner: [
- { value: "google/gemini-3.1-flash-lite", text: "google/gemini-3.1-flash-lite (默认)" },
- { value: "google/gemini-2.5-flash", text: "google/gemini-2.5-flash" },
- { value: "openai/gpt-4o", text: "openai/gpt-4o" },
- { value: "anthropic/claude-3.5-sonnet", text: "anthropic/claude-3.5-sonnet" }
- ],
- claude_sdk: [
- { value: "claude-sonnet-4-6", text: "claude-sonnet-4-6 (默认)" },
- { value: "claude-haiku-4-6", text: "claude-haiku-4-6" }
- ]
- };
- function onProcEngineChange() {
- const engine = document.getElementById("procEngineSelect").value;
- const modelSelect = document.getElementById("procModelSelect");
- modelSelect.innerHTML = ENGINE_MODELS[engine].map(m => `<option value="${m.value}">${m.text}</option>`).join("");
- }
- function getShortCaseFolder(form, case_id) {
- const match = case_id.match(/^([a-z]+)_([0-9a-f]{8})/i);
- const short = match ? `${match[1]}_${match[2]}` : case_id.substring(0, 20);
- return `${form}_${short}`;
- }
- function checkProcedureState(it) {
- if (procPollInterval) {
- clearInterval(procPollInterval);
- procPollInterval = null;
- }
-
- currentProcTask = {
- q: it.run,
- form: st.form,
- case_id: it.case_id,
- procedure_html: it.procedure_html
- };
-
- isLogViewActive = false;
- document.getElementById("procStatusText").textContent = "正在检测工序状态...";
- document.getElementById("procActionBtns").innerHTML = "";
-
- document.getElementById("procSetupPanel").style.display = "none";
- document.getElementById("procConsolePanel").style.display = "none";
- document.getElementById("procedureIframe").style.display = "none";
-
- const url = `/api/procedure_status?q=${it.run}&form=${st.form}&case_id=${it.case_id}`;
- fetch(url)
- .then(r => r.json())
- .then(d => {
- if (d.status === "running") {
- showProcConsole("running");
- pollProcLogs();
- } else if (d.status === "success") {
- showProcIframe(it.procedure_html);
- } else if (d.status === "failed") {
- showProcSetup(d.error);
- } else {
- showProcSetup();
- }
- })
- .catch(err => {
- showProcSetup("检查状态失败: " + err);
- });
- }
- function showProcSetup(errorMsg) {
- document.getElementById("procSetupPanel").style.display = "flex";
- document.getElementById("procConsolePanel").style.display = "none";
- document.getElementById("procedureIframe").style.display = "none";
-
- const statusText = document.getElementById("procStatusText");
- if (errorMsg) {
- statusText.innerHTML = `<span style="color:var(--rose)">提取失败: ${esc(errorMsg)}</span>`;
- } else {
- statusText.textContent = "未生成工序";
- }
-
- document.getElementById("procActionBtns").innerHTML = "";
- document.getElementById("startProcBtn").disabled = false;
- document.getElementById("startProcBtn").textContent = "开始提取工序";
-
- onProcEngineChange();
- }
-
- function showProcConsole(status) {
- document.getElementById("procSetupPanel").style.display = "none";
- document.getElementById("procConsolePanel").style.display = "flex";
- document.getElementById("procedureIframe").style.display = "none";
-
- const statusText = document.getElementById("procStatusText");
- const consoleStatus = document.getElementById("procConsoleStatus");
-
- if (status === "running") {
- statusText.innerHTML = `<span>⏳ 正在提取工序中...</span>`;
- consoleStatus.textContent = "running";
- document.getElementById("procActionBtns").innerHTML = `<button class="btn" style="background:#f3f4f6" disabled>正在生成...</button>`;
- } else if (status === "failed") {
- statusText.innerHTML = `<span style="color:var(--rose)">❌ 提取失败</span>`;
- consoleStatus.textContent = "failed";
- document.getElementById("procActionBtns").innerHTML = `
- <button class="btn" onclick="toggleCompletedLogs()">查看日志</button>
- <button class="btn" style="background:var(--rose); color:#fff; border-color:var(--rose)" onclick="resetToSetup()">重试提取</button>
- `;
- }
- }
-
- function resetToSetup() {
- showProcSetup();
- }
-
- function showProcIframe(htmlPath) {
- document.getElementById("procSetupPanel").style.display = "none";
- document.getElementById("procConsolePanel").style.display = "none";
-
- const iframe = document.getElementById("procedureIframe");
- iframe.style.display = "block";
- iframe.src = "/" + htmlPath;
-
- document.getElementById("procStatusText").innerHTML = `<span style="color:var(--mint)">✓ 工序已生成</span>`;
-
- document.getElementById("procActionBtns").innerHTML = `
- <button class="btn" id="btnToggleLogs" onclick="toggleCompletedLogs()">📋 查看提取日志</button>
- <button class="btn" style="background:var(--rose); color:#fff; border-color:var(--rose);" onclick="regenerateProcedureConfirm()">♻️ 重新生成</button>
- `;
- }
-
- function toggleCompletedLogs() {
- const iframe = document.getElementById("procedureIframe");
- const consolePanel = document.getElementById("procConsolePanel");
- const toggleBtn = document.getElementById("btnToggleLogs");
-
- if (isLogViewActive) {
- consolePanel.style.display = "none";
- iframe.style.display = "block";
- if (toggleBtn) toggleBtn.textContent = "📋 查看提取日志";
- isLogViewActive = false;
- } else {
- iframe.style.display = "none";
- consolePanel.style.display = "flex";
- if (toggleBtn) toggleBtn.textContent = "👁️ 返回工序效果";
- isLogViewActive = true;
- fetchLogsOnce();
- }
- }
-
- function fetchLogsOnce() {
- const consoleOutput = document.getElementById("procConsoleOutput");
- consoleOutput.textContent = "正在加载日志...";
- const url = `/api/procedure_log?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
- fetch(url)
- .then(r => r.json())
- .then(d => {
- consoleOutput.textContent = d.log || "没有提取日志。";
- consoleOutput.scrollTop = consoleOutput.scrollHeight;
- })
- .catch(err => {
- consoleOutput.textContent = "加载日志失败: " + err;
- });
- }
- function startProcedureExtraction() {
- const engine = document.getElementById("procEngineSelect").value;
- const model = document.getElementById("procModelSelect").value;
-
- const startBtn = document.getElementById("startProcBtn");
- startBtn.disabled = true;
- startBtn.textContent = "正在启动...";
-
- const payload = {
- q: currentProcTask.q,
- form: currentProcTask.form,
- case_id: currentProcTask.case_id,
- engine: engine,
- model: model
- };
-
- fetch("/api/generate_procedure", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload)
- })
- .then(r => r.json().then(d => ({ ok: r.ok, d })))
- .then(({ ok, d }) => {
- if (ok && d.status === "started") {
- showProcConsole("running");
- pollProcLogs();
- } else {
- showProcSetup(d.error || "启动失败");
- }
- })
- .catch(err => {
- showProcSetup("网络请求错误: " + err);
- });
- }
-
- function regenerateProcedureConfirm() {
- if (confirm("确定要重新提取并生成工序吗?这会覆盖原有的工序文件和修改记录。")) {
- showProcSetup();
- }
- }
-
- function pollProcLogs() {
- if (procPollInterval) {
- clearInterval(procPollInterval);
- }
-
- const consoleOutput = document.getElementById("procConsoleOutput");
- consoleOutput.textContent = "正在连接后台提取进程...\n";
-
- procPollInterval = setInterval(() => {
- const statusUrl = `/api/procedure_status?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
- fetch(statusUrl)
- .then(r => r.json())
- .then(d => {
- if (d.status === "success") {
- clearInterval(procPollInterval);
- procPollInterval = null;
- loadData(true);
- if (d.procedure_html) {
- showProcIframe(d.procedure_html);
- } else {
- const folder = getShortCaseFolder(currentProcTask.form, currentProcTask.case_id);
- showProcIframe(`runs_full/${currentProcTask.q}/procedures/${folder}/case-${currentProcTask.case_id}.html`);
- }
- } else if (d.status === "failed") {
- clearInterval(procPollInterval);
- procPollInterval = null;
- showProcConsole("failed");
- }
- });
-
- const logUrl = `/api/procedure_log?q=${currentProcTask.q}&form=${currentProcTask.form}&case_id=${currentProcTask.case_id}`;
- fetch(logUrl)
- .then(r => r.json())
- .then(d => {
- if (d.log) {
- const scrollAtBottom = consoleOutput.scrollTop + consoleOutput.clientHeight >= consoleOutput.scrollHeight - 50;
- consoleOutput.textContent = d.log;
- if (scrollAtBottom) {
- consoleOutput.scrollTop = consoleOutput.scrollHeight;
- }
- }
- });
- }, 2000);
- }
- function switchModalTab(tabName) {
- const detailTab = document.getElementById("tabDetailBtn");
- const procTab = document.getElementById("tabProcedureBtn");
- const toolsTab = document.getElementById("tabToolsBtn");
- const detailContent = document.getElementById("modalContentDetail");
- const procContent = document.getElementById("modalContentProcedure");
- const toolsContent = document.getElementById("modalContentTools");
- // 全部复位
- [detailTab, procTab, toolsTab].forEach(t => t && t.classList.remove("active"));
- detailContent.style.display = "none";
- procContent.style.display = "none";
- if (toolsContent) toolsContent.style.display = "none";
- if (tabName === 'procedure') {
- procTab.classList.add("active");
- procContent.style.display = "flex";
- } else if (tabName === 'tools') {
- toolsTab.classList.add("active");
- toolsContent.style.display = "block";
- loadToolsForCurrent(); // 切到工具 tab 时按需加载
- } else {
- detailTab.classList.add("active");
- detailContent.style.display = "grid";
- }
- }
- // fixed_query_eval:图片画廊灯箱(dialog 叠在详情 modal 上层,支持 ←→ 切换)
- let _lbImgs = [], _lbIdx = 0;
- function openLightbox(src) {
- const it = _curDetailItem();
- _lbImgs = (it && Array.isArray(it.images) && it.images.length) ? it.images.slice() : [src];
- _lbIdx = Math.max(0, _lbImgs.indexOf(src));
- _lbRender();
- document.getElementById('imgLightbox').showModal();
- }
- function _lbRender() {
- if (!_lbImgs.length) return;
- document.getElementById('imgLightboxImg').src = _lbImgs[_lbIdx];
- const multi = _lbImgs.length > 1;
- document.querySelectorAll('#imgLightbox .lb-nav').forEach(b => b.classList.toggle('hidden', !multi));
- const ctr = document.getElementById('lbCounter');
- ctr.textContent = `${_lbIdx + 1} / ${_lbImgs.length}`;
- ctr.style.display = multi ? '' : 'none';
- }
- function lightboxNav(d) {
- if (_lbImgs.length < 2) return;
- _lbIdx = (_lbIdx + d + _lbImgs.length) % _lbImgs.length; // 循环切换
- _lbRender();
- }
- // 键盘 ←/→ 切换(ESC 关闭走 dialog 默认)
- document.addEventListener('keydown', e => {
- const lb = document.getElementById('imgLightbox');
- if (!lb || !lb.open) return;
- if (e.key === 'ArrowLeft') { e.preventDefault(); lightboxNav(-1); }
- else if (e.key === 'ArrowRight') { e.preventDefault(); lightboxNav(1); }
- });
- // ════════ fixed_query_eval:工具解构(批量选帖 + 单帖 + 结果渲染)════════
- const TOOL_MODEL_LABEL = 'gemini-3.1-flash-lite';
- function _curQueryPosts() {
- const q = DATA.queries[st.qi];
- if (!q) return { key: null, label: '', items: [] };
- const form = q.forms && q.forms[st.fi];
- let items = (form && form.results) || [];
- if (st.channel !== 'all') items = items.filter(r => r.platformKey === st.channel);
- return { key: q.key, label: q.original_q || q.key, items };
- }
- function openToolBatchModal() {
- const { key, label, items } = _curQueryPosts();
- if (!key) { alert('请先选择一个 query'); return; }
- document.getElementById('toolBatchSub').textContent =
- `${label} · ${st.channel === 'all' ? '全部渠道' : (PLATC[st.channel] || st.channel)} · 共 ${items.length} 帖`;
- document.getElementById('toolBatchAll').checked = false;
- document.getElementById('toolBatchList').innerHTML = items.map(it => {
- const img = (it.images && it.images[0]) || '';
- return `<label style="display:flex; gap:10px; align-items:center; padding:8px 4px; border-bottom:1px solid var(--line); cursor:pointer;">
- <input type="checkbox" class="tb-item" value="${esc(it.case_id)}" onchange="toolBatchUpdateCount()">
- ${img ? `<img src="${esc(img)}" referrerpolicy="no-referrer" style="width:42px;height:42px;object-fit:cover;border-radius:6px;flex-shrink:0;" onerror="this.style.opacity=.2">`
- : '<div style="width:42px;height:42px;background:#f1f1f1;border-radius:6px;flex-shrink:0;"></div>'}
- <span class="platform p-${esc(it.platformKey)}" style="flex-shrink:0">${esc(it.platform)}</span>
- <span style="flex:1; font-size:13px; line-height:1.4; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;">${esc(it.title || '(无标题)')}</span>
- </label>`;
- }).join('') || '<p style="color:var(--muted); padding:24px; text-align:center;">该渠道暂无帖子</p>';
- toolBatchUpdateCount();
- toolBatchDialog.showModal();
- }
- function toolBatchToggleAll(on) {
- document.querySelectorAll('#toolBatchList .tb-item').forEach(c => c.checked = on);
- toolBatchUpdateCount();
- }
- function toolBatchUpdateCount() {
- const n = document.querySelectorAll('#toolBatchList .tb-item:checked').length;
- document.getElementById('toolBatchCount').textContent = `已选 ${n} 帖`;
- }
- function confirmToolExtract() {
- const ids = [...document.querySelectorAll('#toolBatchList .tb-item:checked')].map(c => c.value);
- if (!ids.length) { alert('请至少选择一个帖子'); return; }
- const { key } = _curQueryPosts();
- const btn = document.getElementById('toolBatchConfirm');
- btn.disabled = true; btn.textContent = '启动中…';
- fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ q: key, case_ids: ids }) })
- .then(r => r.json()).then(d => {
- btn.disabled = false; btn.textContent = '确认解构';
- if (d.status === 'started') {
- alert(`已开始解构 ${d.count} 帖(${TOOL_MODEL_LABEL})。\n完成后打开帖子详情的「工具解构」tab 查看。`);
- toolBatchDialog.close();
- } else { alert('启动失败:' + (d.error || '未知')); }
- }).catch(e => { btn.disabled = false; btn.textContent = '确认解构'; alert('请求失败:' + e); });
- }
- // ── 详情页「工具解构」tab ──
- function _curDetailItem() { return VIEW[detailDialog.dataset.activeIdx]; }
- let toolVersion = null; // null = 最新版本;切到历史版本时设为具体版本号
- function loadToolsForCurrent() {
- const it = _curDetailItem(); if (!it) return;
- const pane = document.getElementById('modalContentTools');
- pane.innerHTML = '<div style="padding:40px;text-align:center;color:var(--muted)">加载中…</div>';
- const vparam = toolVersion ? `&version=${encodeURIComponent(toolVersion)}` : '';
- fetch(`/api/tools_data?q=${encodeURIComponent(it.run)}&case_id=${encodeURIComponent(it.case_id)}${vparam}`)
- .then(r => r.json()).then(d => { pane.innerHTML = d.exists ? renderToolCards(d) : toolSetupHTML(); requestAnimationFrame(_markClampedCells); })
- .catch(() => { pane.innerHTML = '<div style="padding:40px;color:#c0392b">加载失败</div>'; });
- }
- function changeToolVersion(v) { toolVersion = v || null; loadToolsForCurrent(); }
- function toolSetupHTML() {
- return `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:48px 20px;">
- <div style="font-size:36px;margin-bottom:14px;">🔧</div>
- <h4 style="margin:0 0 8px;font-size:18px;">解构本帖工具</h4>
- <p style="color:var(--muted);font-size:14px;max-width:480px;margin:0 0 20px;">该帖子尚未解构工具信息。将用 <b>${TOOL_MODEL_LABEL}</b> 读取正文 + 配图,提炼每个工具的结构化条目。</p>
- <button onclick="startToolExtractSingle()" style="background:var(--mint);color:#fff;border-color:var(--mint);padding:10px 24px;font-size:15px;font-weight:bold;border-radius:8px;cursor:pointer;">开始解构</button>
- </div>`;
- }
- function _pollToolsThenLoad(it, pane) {
- const timer = setInterval(() => {
- fetch(`/api/tools_status?q=${encodeURIComponent(it.run)}&case_id=${encodeURIComponent(it.case_id)}`)
- .then(r => r.json()).then(s => {
- if (!s.running) {
- clearInterval(timer);
- if (s.error) pane.innerHTML = `<div style="padding:40px;color:#c0392b">解构失败:${esc(s.error)}</div>`;
- else loadToolsForCurrent();
- }
- }).catch(() => {});
- }, 2500);
- }
- function startToolExtractSingle() {
- const it = _curDetailItem(); if (!it) return;
- toolVersion = null; // 新解构 → 完成后看最新版本
- const pane = document.getElementById('modalContentTools');
- pane.innerHTML = `<div style="padding:48px;text-align:center;color:var(--muted)">⏳ 正在解构本帖工具…(${TOOL_MODEL_LABEL})</div>`;
- fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ q: it.run, case_ids: [it.case_id] }) })
- .then(r => r.json()).then(d => {
- if (d.status !== 'started') { pane.innerHTML = `<div style="padding:40px;color:#c0392b">启动失败:${esc(d.error || '未知')}</div>`; return; }
- _pollToolsThenLoad(it, pane);
- });
- }
- function reExtractTools() {
- const it = _curDetailItem(); if (!it) return;
- if (!confirm('重新解构会生成一个新版本(旧版本保留,可在版本下拉里切回),确定?')) return;
- toolVersion = null; // 解构后看最新版本
- const pane = document.getElementById('modalContentTools');
- pane.innerHTML = `<div style="padding:48px;text-align:center;color:var(--muted)">⏳ 重新解构中…(${TOOL_MODEL_LABEL})</div>`;
- fetch('/api/extract_tools', { method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ q: it.run, case_ids: [it.case_id], force: true }) })
- .then(r => r.json()).then(() => _pollToolsThenLoad(it, pane));
- }
- function _toolField(label, val, color) {
- if (val === null || val === undefined || val === '' || (Array.isArray(val) && !val.length)) return '';
- const body = Array.isArray(val)
- ? '<ul style="margin:2px 0 0;padding-left:18px;">' + val.map(x => `<li style="margin:2px 0;">${esc(String(x))}</li>`).join('') + '</ul>'
- : `<span>${esc(String(val))}</span>`;
- return `<div style="margin-top:8px;font-size:13px;line-height:1.5;"><span style="font-size:12px;font-weight:700;color:${color || 'var(--muted)'};">${label}</span> ${body}</div>`;
- }
- // 案例:新结构是 [{输入, 输出, 效果}] 对象数组(旧的可能是字符串,兼容)
- function _renderCaseList(cases) {
- return (cases || []).map(c => {
- if (c === null || typeof c !== 'object') return `<li style="margin:2px 0;">${esc(String(c))}</li>`;
- const parts = [];
- if (c['输入']) parts.push(`<b>输入</b>:${esc(c['输入'])}`);
- if (c['输出']) parts.push(`<b>输出</b>:${esc(c['输出'])}`);
- if (c['效果']) parts.push(`<b>效果</b>:${esc(c['效果'])}`);
- return `<li style="margin:3px 0;">${parts.join(';') || esc(JSON.stringify(c))}</li>`;
- }).join('');
- }
- function _renderCaseBlock(cases) { // 卡片视图用
- if (!Array.isArray(cases) || !cases.length) return '';
- return `<div style="margin-top:8px;font-size:13px;line-height:1.5;"><span style="font-size:12px;font-weight:700;color:var(--muted);">案例</span><ul style="margin:2px 0 0;padding-left:18px;">${_renderCaseList(cases)}</ul></div>`;
- }
- let toolViewMode = 'table'; // 'cards' | 'table'(默认表格)
- let _lastToolsData = null;
- function renderToolCards(d) {
- _lastToolsData = d;
- const tools = d.tools || [];
- const toggleLabel = toolViewMode === 'table' ? '▤ 卡片视图' : '⊞ 表格视图';
- const versions = d.versions || [];
- const curVer = d.version || versions[0] || '';
- const verSel = versions.length
- ? `<select onchange="changeToolVersion(this.value)" title="切换历史版本" style="font-size:12px;padding:3px 8px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer;">${versions.map(v => `<option value="${esc(v)}" ${v === curVer ? 'selected' : ''}>${esc(v)}${v === versions[0] ? '(最新)' : ''}</option>`).join('')}</select>`
- : (curVer ? `<span style="font-size:12px;color:var(--muted);">版本 ${esc(curVer)}</span>` : '');
- const header = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;gap:10px;">
- <div style="font-size:14px;color:var(--muted);">共 <b style="color:var(--ink);">${tools.length}</b> 个工具 · 模型 ${esc(d.model || TOOL_MODEL_LABEL)}</div>
- <div style="display:flex;gap:8px;align-items:center;flex-shrink:0;">
- ${verSel}
- <button onclick="toggleToolView()" style="font-size:12px;">${toggleLabel}</button>
- <button onclick="reExtractTools()" style="font-size:12px;">↻ 重新解构</button>
- </div>
- </div>`;
- if (!tools.length) return header + '<p style="color:var(--muted);padding:30px;text-align:center;">未识别到工具。</p>';
- return header + (toolViewMode === 'table' ? renderToolTable(tools) : renderToolCardsBody(tools));
- }
- function toggleToolView() {
- toolViewMode = toolViewMode === 'table' ? 'cards' : 'table';
- if (_lastToolsData) {
- document.getElementById('modalContentTools').innerHTML = renderToolCards(_lastToolsData);
- requestAnimationFrame(_markClampedCells);
- }
- }
- function renderToolCardsBody(tools) {
- return tools.map(t => {
- const layer = t['创作层级'] || '';
- const lc = layer === '制作层' ? '#0e7490' : (layer === '创作层' ? '#b87918' : '#9aa0ad');
- const lbg = layer === '制作层' ? '#e0f2f1' : (layer === '创作层' ? '#fef3e2' : '#f1f1f1');
- const tags = [];
- if (t['实质作用域']) tags.push(`<span class="tag">实质:${esc(t['实质作用域'])}</span>`);
- if (t['形式作用域']) tags.push(`<span class="tag">形式:${esc(t['形式作用域'])}</span>`);
- const updated = t['最新更新时间'] ? `<span style="font-size:12px;color:var(--muted);">⏱ ${esc(t['最新更新时间'])}</span>` : '';
- const link = t['来源链接'] ? `<div style="margin-top:8px;"><a href="${esc(t['来源链接'])}" target="_blank" style="font-size:13px;color:#2563eb;">🔗 来源链接</a></div>` : '';
- return `<div style="border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:12px;background:#fff;box-shadow:var(--shadow);">
- <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
- <h4 style="margin:0;font-size:16px;">🔧 ${esc(t['工具名称'] || '(未命名)')}</h4>
- ${layer ? `<span style="font-size:12px;font-weight:700;color:${lc};background:${lbg};padding:2px 10px;border-radius:20px;flex-shrink:0;">${esc(layer)}</span>` : ''}
- </div>
- ${(tags.length || updated) ? `<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-top:8px;">${tags.join('')} ${updated}</div>` : ''}
- <div style="display:flex;gap:28px;flex-wrap:wrap;">${_toolField('输入', t['输入'])}${_toolField('输出', t['输出'])}</div>
- ${_toolField('用法', t['用法'])}
- ${_renderCaseBlock(t['案例'])}
- ${_toolField('缺点', t['缺点'], '#b8651a')}
- ${link}
- </div>`;
- }).join('');
- }
- // 表格视图:列对应数据库 fqe_tools 字段
- function _toolCell(v) {
- if (v === null || v === undefined || v === '' || (Array.isArray(v) && !v.length)) return '<span style="color:#c4c4c4">—</span>';
- if (Array.isArray(v)) return '<ul style="margin:0;padding-left:15px;">' + v.map(x => `<li style="margin:1px 0;">${esc(String(x))}</li>`).join('') + '</ul>';
- return esc(String(v));
- }
- const DASH = '<span class="dash">—</span>';
- // 单元格内容包一层 .tcell(可限高+点击展开);不可点的列(链接/徽章/工具名)不包
- function _ttCell(inner, clampable) {
- return clampable ? `<div class="tcell" onclick="this.classList.toggle('open')">${inner}</div>` : inner;
- }
- // 普通列(案例 group 之外)的单元格内容
- function _toolCellContent(c, t) {
- let inner, cls = '', clampable = true, style = '';
- if (c === '工具名称') {
- cls = 'col-tool'; clampable = false; inner = `🔧 ${esc(t[c] || '(未命名)')}`;
- } else if (c === '来源链接') {
- clampable = false;
- inner = t[c] ? `<a href="${esc(t[c])}" target="_blank" style="color:#176d64;font-weight:600;">🔗 打开</a>` : DASH;
- } else if (c === '创作层级') {
- clampable = false;
- inner = t[c] ? `<span class="layer-badge ${t[c] === '制作层' ? 'make' : 'create'}">${esc(t[c])}</span>` : DASH;
- } else {
- const v = _toolCell(t[c]);
- inner = (v === '<span style="color:#c4c4c4">—</span>') ? DASH : v;
- }
- if (['输入', '输出', '用法', '缺点'].includes(c)) style = 'max-width:240px;';
- else if (!clampable) style = 'white-space:nowrap;';
- return { inner, cls, clampable, style };
- }
- function _td(c, t, rowspan) {
- const { inner, cls, clampable, style } = _toolCellContent(c, t);
- const rs = rowspan > 1 ? ` rowspan="${rowspan}"` : '';
- return `<td class="${cls}" style="${style}"${rs}>${_ttCell(inner, clampable)}</td>`;
- }
- function _caseTd(cse, key) {
- const v = (cse && cse[key] != null && cse[key] !== '') ? esc(String(cse[key])) : DASH;
- return `<td class="col-case" style="max-width:210px;">${_ttCell(v, true)}</td>`;
- }
- function renderToolTable(tools) {
- // 案例 group(输入/输出/效果)放在 用法 后、缺点 前;用 colspan/rowspan 做两层表头
- const before = ['工具名称', '创作层级', '实质作用域', '形式作用域', '输入', '输出', '用法'];
- const after = ['缺点', '来源链接', '最新更新时间'];
- const thead = `<thead>
- <tr>
- ${before.map(c => `<th rowspan="2">${c}</th>`).join('')}
- <th colspan="3" class="th-group">案例</th>
- ${after.map(c => `<th rowspan="2">${c}</th>`).join('')}
- </tr>
- <tr>${['输入', '输出', '效果'].map(c => `<th class="th-sub">${c}</th>`).join('')}</tr>
- </thead>`;
- const rows = tools.map((t, ti) => {
- const cases = (Array.isArray(t['案例']) && t['案例'].length) ? t['案例'] : [null];
- const K = cases.length;
- const par = ti % 2 ? 'tr-b' : 'tr-a';
- return cases.map((cse, i) => {
- const caseTds = `${_caseTd(cse, '输入')}${_caseTd(cse, '输出')}${_caseTd(cse, '效果')}`;
- if (i === 0) {
- return `<tr class="${par}">${before.map(c => _td(c, t, K)).join('')}${caseTds}${after.map(c => _td(c, t, K)).join('')}</tr>`;
- }
- return `<tr class="${par}">${caseTds}</tr>`;
- }).join('');
- }).join('');
- return `<div class="fqe-ttwrap"><table class="fqe-tt fqe-tt2">${thead}<tbody>${rows}</tbody></table></div>`;
- }
- // 渲染后标记真正溢出的单元格(才显示蒙版+可点击)
- function _markClampedCells() {
- document.querySelectorAll('#modalContentTools .fqe-tt .tcell').forEach(el => {
- if (!el.classList.contains('open') && el.scrollHeight > el.clientHeight + 2) el.classList.add('clamped');
- else if (el.scrollHeight <= el.clientHeight + 2) el.classList.remove('clamped');
- });
- }
- function openDetail(i) {
- const it = VIEW[i];
- detailDialog.dataset.activeIdx = i;
- toolVersion = null; // 新帖 → 工具 tab 默认看最新版本
- currentPinnedScoreEl = null;
- document.getElementById("modalMeta").innerHTML = `<span class="platform p-${esc(it.platformKey)}">${esc(it.platform)}</span><span>Query ID: <b style="font-family: monospace; color: var(--amber); background: var(--soft-amber); border: 1px solid rgba(184, 121, 24, 0.2); padding: 1px 6px; border-radius: 3px; font-size: 11px; margin-right: 8px;">${esc(it.run)}</b></span><span>${esc(it.date)} · ${esc(it.engagement)} · 质量 ${esc(it.grade)} ${esc(it.qscore)}</span>`;
- document.getElementById("modalTitle").textContent = it.title;
- document.getElementById("modalReason").textContent = it.reason;
- document.getElementById("modalText").textContent = it.text || "(无正文)";
- document.getElementById("modalImages").innerHTML = it.images.length ? it.images.map(s => `<img src="${esc(s)}" referrerpolicy="no-referrer" loading="lazy" style="cursor:zoom-in" onclick="openLightbox(this.src)" onerror="this.style.opacity=.3">`).join("") : "<p>搜索详情未返回图片。</p>";
- document.getElementById("modalTags").innerHTML = [...(it.knowledge_type || []).map(t => "类型:" + (KTM[t] || t)), ...(it.found_by || []).map(q => "命中:" + q)].map(t => `<span class="tag">${esc(t)}</span>`).join("");
-
- const isNewSchema = it.scores && (it.scores.relevance_production !== undefined || it.scores.relevance_query !== undefined);
-
- const maxScore = isNewSchema ? '10' : '5';
- const overallVal = it.anomaly ? '—' : it.overall.toFixed(1);
- const scoreColor = isNewSchema ? '#2563eb' : 'var(--mint)';
- const scoreStrong = document.getElementById("modalOverallScoreVal");
- if (scoreStrong) {
- scoreStrong.style.color = scoreColor;
- scoreStrong.innerHTML = `${overallVal}<span style="font-size: 12px; color: var(--muted); font-weight: 500; margin-left: 2px;">/${maxScore}</span>`;
- }
- if (isNewSchema) {
- document.getElementById("modalScores").innerHTML = renderNewScores(it);
- } else {
- document.getElementById("modalScores").innerHTML = scoreGroupsOld.map(g => renderScoreGroup(it, g)).join("");
- }
-
- const tabs = document.getElementById("modalTabs");
- tabs.style.display = "flex";
-
- checkProcedureState(it);
-
- switchModalTab('detail');
- detailDialog.showModal();
- }
- let currentPinnedScoreEl = null;
- function pinScoreReason(el, label, k) {
- const activeIdx = detailDialog.dataset.activeIdx;
- const it = VIEW[activeIdx];
- if (!it || !it.score_reasons) return;
- const reason = it.score_reasons[k] || '';
- let tip = document.getElementById('scoreTip');
- if (!tip) {
- tip = document.createElement('div');
- tip.id = 'scoreTip';
- tip.style.cssText = 'position: fixed; background: #ffffff; border: 1px solid #ded8ce; border-radius: 8px; box-shadow: 0 10px 25px rgba(41, 35, 28, 0.15); padding: 12px 14px; max-width: 320px; z-index: 99999; font-size: 13px; line-height: 1.5; color: #4b5563; word-break: break-all; border-left: 4px solid var(--amber); transition: opacity 0.15s ease;';
- detailDialog.appendChild(tip);
- }
- if (currentPinnedScoreEl === el && tip.style.display === 'block') {
- tip.style.display = 'none';
- el.style.color = '';
- currentPinnedScoreEl = null;
- } else {
- if (currentPinnedScoreEl) {
- currentPinnedScoreEl.style.color = '';
- }
- tip.innerHTML = `<strong style="display:block;margin-bottom:6px;color:#b87918;">【指标判定 - ${label}】</strong>${esc(reason)}`;
- tip.style.display = 'block';
- el.style.color = 'var(--amber)';
- currentPinnedScoreEl = el;
- const elRect = el.getBoundingClientRect();
- const tipWidth = tip.offsetWidth || 280;
- const tipHeight = tip.offsetHeight || 80;
- let left = elRect.left + elRect.width / 2 - tipWidth / 2;
- let top = elRect.bottom + 8;
- if (left < 10) left = 10;
- if (left + tipWidth > window.innerWidth - 10) {
- left = window.innerWidth - tipWidth - 10;
- }
- if (top + tipHeight > window.innerHeight - 10) {
- top = elRect.top - tipHeight - 8;
- }
- tip.style.left = left + 'px';
- tip.style.top = top + 'px';
- }
- }
- // Automatically close tooltip and clear interval when modal is closed
- document.getElementById('detailDialog').addEventListener('close', () => {
- const tip = document.getElementById('scoreTip');
- if (tip) {
- tip.style.display = 'none';
- }
- if (currentPinnedScoreEl) {
- currentPinnedScoreEl.style.color = '';
- currentPinnedScoreEl = null;
- }
- if (procPollInterval) {
- clearInterval(procPollInterval);
- procPollInterval = null;
- }
- });
- // Close tooltip when dialog scrolls
- document.getElementById('detailDialog').addEventListener('scroll', () => {
- const tip = document.getElementById('scoreTip');
- if (tip) {
- tip.style.display = 'none';
- }
- if (currentPinnedScoreEl) {
- currentPinnedScoreEl.style.color = '';
- currentPinnedScoreEl = null;
- }
- }, true);
- const KTM = { procedure: "工序", step: "步骤", tool: "工具" };
- function rerender(mxClick) {
- updateThresholdLimits();
- if (st.qi === -1) {
- document.getElementById("stats").innerHTML = "";
- document.getElementById("lede").textContent = "未找到对应的数据库 Query,请尝试切换筛选组合。";
- renderFormsChan();
- renderGrid();
- if (!mxClick) {
- document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
- }
- return;
- }
- if (st.qi >= DATA.queries.length) st.qi = 0;
- if (st.fi >= DATA.queries[st.qi].forms.length) st.fi = 0;
- if (!mxClick) {
- renderMatrix();
- }
- renderFormsChan();
- renderHead();
- renderGrid();
- // Sync reevalBtn state
- const reevalBtn = document.getElementById('reevalBtn');
- if (reevalBtn && DATA.queries[st.qi]) {
- const q = DATA.queries[st.qi].key;
- const status = DATA.active_reevals && DATA.active_reevals[q];
- if (status === "running") {
- reevalBtn.disabled = true;
- reevalBtn.textContent = `♻️ 重评中 ${q}...`;
- startReevalPolling(q);
- } else {
- reevalBtn.disabled = false;
- reevalBtn.textContent = '♻️ 重评当前 query';
- }
- }
- }
- function setMatrixView(mode) {
- st.matrixView = mode;
- document.querySelectorAll('.g-mxview .btn').forEach(btn => {
- btn.classList.remove('on');
- });
- if (mode === 'full') document.getElementById('btnMxFull').classList.add('on');
- else if (mode === 'hits') document.getElementById('btnMxHits').classList.add('on');
- else if (mode === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
-
- if (st.selectedAction && st.selectedType) {
- selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
- }
- renderMatrix();
- if (openCell) {
- const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
- const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
- if (newCell) {
- showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
- }
- }
- rerender(true);
- }
- function loadData(keep) {
- fetch("/api/data").then(r => r.json()).then(d => {
- DATA = d;
- if (!keep) {
- st = { form: 'A', lens: '工序', tools: [], tier: 0, qi: 0, fi: 0, channel: "all", matrixView: 'full', selectedAction: null, selectedType: null };
- if (DATA.queries.length > 0) {
- const firstQ = DATA.queries[0];
- if (firstQ.dims) {
- st.selectedAction = firstQ.dims.action;
- st.selectedType = firstQ.dims.type;
- st.lens = detectLens(firstQ);
- }
- }
- }
- // Dynamically populate tool type buttons
- document.getElementById('toolBtns').innerHTML = TOOL_TYPES.map(t => `<button class="btn" data-k="tool" data-v="${t}">${t}</button>`).join('');
- // Synchronize buttons states to st
- document.querySelectorAll('.ctl .btn').forEach(btn => {
- const k = btn.dataset.k, v = btn.dataset.v;
- if (MULTI[k]) {
- const ak = MULTI[k];
- btn.classList.toggle('on', v === '' ? st[ak].length === 0 : st[ak].includes(v));
- } else {
- btn.classList.toggle('on', st[k] === v);
- }
- });
- document.querySelectorAll('.g-mxview .btn').forEach(btn => {
- btn.classList.remove('on');
- });
- const mv = st.matrixView || 'full';
- if (mv === 'full') document.getElementById('btnMxFull').classList.add('on');
- else if (mv === 'hits') document.getElementById('btnMxHits').classList.add('on');
- else if (mv === 'procedures') document.getElementById('btnMxProcedures').classList.add('on');
- computeStats();
- if (st.selectedAction && st.selectedType) {
- selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
- }
- rerender();
- });
- }
- // Matrix click listener (links matrix select to database select + opens pop-up)
- document.getElementById("comboMx").addEventListener("click", e => {
- const td = e.target.closest("td.cell");
- if (!td) return;
- const ai = +td.dataset.ai, ti = +td.dataset.ti, a = ACTIONS[ai], t = TYPES[ti];
- st.selectedAction = a.name;
- st.selectedType = t.name;
- showPop(td, e.clientX, e.clientY);
- // Select the query
- selectQueryByActiveCellAndControls(a.name, t.name);
- document.querySelectorAll('td.cell.sel').forEach(x => x.classList.remove('sel'));
- td.classList.add('sel');
- applyCrosshair();
- rerender(true); // mxClick = true, updates results without redrawing full table
- });
- // Header controls event delegation
- const MULTI = { tool: 'tools' };
- document.querySelector('.ctl').addEventListener('click', e => {
- const b = e.target.closest('.btn');
- if (!b) return;
- const k = b.dataset.k, v = b.dataset.v, grp = document.querySelectorAll(`.ctl .btn[data-k="${k}"]`);
- if (MULTI[k]) {
- const ak = MULTI[k];
- if (v === '') {
- st[ak] = [];
- } else {
- if (st[ak].includes(v)) {
- st[ak] = [];
- } else {
- st[ak] = [v];
- }
- }
- grp.forEach(x => {
- const xv = x.dataset.v;
- x.classList.toggle('on', xv === '' ? st[ak].length === 0 : st[ak].includes(xv));
- });
- } else {
- grp.forEach(x => x.classList.remove('on'));
- b.classList.add('on');
- st[k] = v;
- }
- if (st.selectedAction && st.selectedType) {
- selectQueryByActiveCellAndControls(st.selectedAction, st.selectedType);
- }
- renderMatrix();
- if (openCell) {
- const ai = +openCell.dataset.ai, ti = +openCell.dataset.ti;
- const newCell = document.querySelector(`td.cell[data-ai="${ai}"][data-ti="${ti}"]`);
- if (newCell) {
- showPop(newCell, parseInt(pop.style.left), parseInt(pop.style.top));
- }
- }
- rerender(true);
- });
- // Close popups on clicking outside
- document.addEventListener('click', e => {
- if (!e.target.closest('td.cell') && !e.target.closest('.pop') && !e.target.closest('.btn')) {
- pop.style.display = 'none';
- openCell = null;
- }
- const tip = document.getElementById('scoreTip');
- if (tip && !e.target.closest('.info-icon') && !e.target.closest('#scoreTip')) {
- tip.style.display = 'none';
- if (currentPinnedScoreEl) {
- currentPinnedScoreEl.style.color = '';
- currentPinnedScoreEl = null;
- }
- }
- });
- document.getElementById("navC").addEventListener("click", e => { const t = e.target.closest(".tab"); if (!t) return; st.channel = t.dataset.c; renderNav(); renderHead(); renderGrid(); });
- document.getElementById("sort").addEventListener("change", renderGrid);
- // ── fixed_query_eval:固定 query 选择条(替代正交矩阵导航)─────────────────────
- function renderNavQ() {
- const el = document.getElementById('navQ'); if (!el) return;
- const qs = (DATA && DATA.queries) || [];
- el.innerHTML = qs.length
- ? qs.map((q, i) => `<span class="tab ${i === st.qi ? 'on' : ''}" data-qi="${i}">${q.original_q || q.key} <small>${q.tot || 0}</small></span>`).join('')
- : '<span style="color:var(--muted)">runs_full/ 暂无数据,先跑 run_search.py</span>';
- }
- document.getElementById('navQ').addEventListener('click', e => {
- const t = e.target.closest('.tab'); if (!t) return;
- st.qi = +t.dataset.qi; st.fi = 0; st.form = 'A'; st.channel = 'all';
- rerender(true); renderNavQ();
- });
- // 包一层 rerender:每次渲染后同步刷新 query 选择条高亮
- const _origRerender = rerender;
- rerender = function () { const r = _origRerender.apply(this, arguments); renderNavQ(); return r; };
- loadData();
- </script>
- </body>
- </html>
|