visualize_inspiration_points.py 135 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214
  1. """
  2. 灵感点分析结果可视化脚本
  3. 读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面
  4. """
  5. import json
  6. from pathlib import Path
  7. from typing import Dict, Any, List, Optional
  8. from datetime import datetime
  9. import html as html_module
  10. def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
  11. """
  12. 加载所有灵感点的分析结果
  13. Args:
  14. inspiration_dir: 灵感点目录路径
  15. Returns:
  16. 灵感点分析结果列表
  17. """
  18. inspiration_path = Path(inspiration_dir)
  19. results = []
  20. # 遍历所有子目录
  21. for subdir in inspiration_path.iterdir():
  22. if subdir.is_dir():
  23. # 查找 all_summary 文件
  24. summary_files = list(subdir.glob("all_summary_*.json"))
  25. if summary_files:
  26. summary_file = summary_files[0]
  27. try:
  28. with open(summary_file, 'r', encoding='utf-8') as f:
  29. data = json.load(f)
  30. # 加载完整的 step1 和 step2 数据
  31. step1_data = None
  32. step2_data = None
  33. # 直接从当前子目录查找 step1 和 step2 文件
  34. step1_files = list(subdir.glob("all_step1_*.json"))
  35. step2_files = list(subdir.glob("all_step2_*.json"))
  36. if step1_files:
  37. try:
  38. with open(step1_files[0], 'r', encoding='utf-8') as f:
  39. step1_data = json.load(f)
  40. except Exception as e:
  41. print(f"警告: 读取 {step1_files[0]} 失败: {e}")
  42. if step2_files:
  43. try:
  44. with open(step2_files[0], 'r', encoding='utf-8') as f:
  45. step2_data = json.load(f)
  46. except Exception as e:
  47. print(f"警告: 读取 {step2_files[0]} 失败: {e}")
  48. # 加载搜索结果和匹配分数
  49. search_results = {}
  50. search_dir = subdir / "search"
  51. if search_dir.exists() and search_dir.is_dir():
  52. search_files = list(search_dir.glob("all_search_*.json"))
  53. for search_file in search_files:
  54. try:
  55. with open(search_file, 'r', encoding='utf-8') as f:
  56. search_data = json.load(f)
  57. # 从JSON内容中读取真实的keyword,而不是从文件名提取
  58. keyword = search_data.get("search_params", {}).get("keyword", "")
  59. if keyword:
  60. # 尝试加载对应的匹配结果文件
  61. match_file = search_dir / "all_step4_搜索结果匹配_gemini-2.5-pro.json"
  62. match_data = None
  63. if match_file.exists():
  64. try:
  65. with open(match_file, 'r', encoding='utf-8') as mf:
  66. match_data = json.load(mf)
  67. except Exception as e:
  68. print(f"警告: 读取匹配文件 {match_file} 失败: {e}")
  69. search_results[keyword] = {
  70. "search_data": search_data,
  71. "match_data": match_data
  72. }
  73. else:
  74. # 如果JSON中没有keyword,则从文件名提取
  75. keyword = search_file.stem.replace("all_search_", "")
  76. search_results[keyword] = {
  77. "search_data": search_data,
  78. "match_data": None
  79. }
  80. except Exception as e:
  81. print(f"警告: 读取 {search_file} 失败: {e}")
  82. results.append({
  83. "summary": data,
  84. "step1": step1_data,
  85. "step2": step2_data,
  86. "search_results": search_results,
  87. "inspiration_name": subdir.name
  88. })
  89. except Exception as e:
  90. print(f"警告: 读取 {summary_file} 失败: {e}")
  91. return results
  92. def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]:
  93. """
  94. 加载所有帖子详情数据
  95. Args:
  96. posts_dir: 帖子目录路径
  97. Returns:
  98. 帖子ID到帖子详情的映射
  99. """
  100. posts_path = Path(posts_dir)
  101. posts_map = {}
  102. for post_file in posts_path.glob("*.json"):
  103. try:
  104. with open(post_file, 'r', encoding='utf-8') as f:
  105. post_data = json.load(f)
  106. post_id = post_data.get("channel_content_id")
  107. if post_id:
  108. posts_map[post_id] = post_data
  109. except Exception as e:
  110. print(f"警告: 读取 {post_file} 失败: {e}")
  111. return posts_map
  112. def generate_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-post", post_to_mapping_data: Dict[str, Any] = None) -> str:
  113. """
  114. 生成单个帖子卡片HTML(与搜索结果样式一致)
  115. Args:
  116. post: 帖子数据
  117. note_id_prefix: 帖子ID前缀,用于区分不同区域的帖子
  118. post_to_mapping_data: 帖子到分类和点映射数据
  119. Returns:
  120. HTML字符串
  121. """
  122. import html as html_module
  123. import random
  124. title = post.get("title", "")
  125. desc = post.get("body_text", "")
  126. images = post.get("images", [])
  127. like_count = post.get("like_count", 0)
  128. comment_count = post.get("comment_count", 0)
  129. link = post.get("link", "")
  130. author = post.get("channel_account_name", "")
  131. post_id = post.get("channel_content_id", "")
  132. publish_time = post.get("publish_time", "")
  133. # 生成唯一的note_id
  134. note_id = f"{note_id_prefix}-{random.randint(10000, 99999)}"
  135. # 生成图片轮播HTML
  136. images_html = ""
  137. if images and len(images) > 0:
  138. images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
  139. # 图片导航按钮
  140. nav_buttons = ""
  141. if len(images) > 1:
  142. nav_buttons = f'''
  143. <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
  144. <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
  145. '''
  146. images_html = f'''
  147. <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
  148. <div class="note-images-track" id="{note_id}-track">
  149. {images_track}
  150. </div>
  151. {nav_buttons}
  152. </div>
  153. '''
  154. else:
  155. # 无图片时显示占位符
  156. images_html = f'''
  157. <div class="note-image-carousel">
  158. <div class="note-images-track">
  159. <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
  160. 暂无图片
  161. </div>
  162. </div>
  163. </div>
  164. '''
  165. # 准备详情数据
  166. note_data = {
  167. "title": title,
  168. "desc": desc,
  169. "images": images,
  170. "link": link,
  171. "author": author,
  172. "like_count": like_count,
  173. "comment_count": comment_count
  174. }
  175. # 添加灵感点、关键点、目的点到note_data
  176. if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
  177. mapping = post_to_mapping_data[post_id]
  178. note_data["inspiration_points"] = mapping.get("灵感点列表", [])
  179. note_data["key_points"] = mapping.get("关键点列表", [])
  180. note_data["purpose_points"] = mapping.get("目的点列表", [])
  181. import json
  182. note_data_json = json.dumps(note_data, ensure_ascii=False)
  183. note_data_json_escaped = html_module.escape(note_data_json)
  184. # 获取帖子的灵感点、关键点、目的点
  185. points_html = ""
  186. if post_to_mapping_data and post_id and post_id in post_to_mapping_data:
  187. mapping = post_to_mapping_data[post_id]
  188. inspiration_points = mapping.get("灵感点列表", [])
  189. key_points = mapping.get("关键点列表", [])
  190. purpose_points = mapping.get("目的点列表", [])
  191. points_sections = []
  192. # 灵感点
  193. if inspiration_points:
  194. insp_items = "".join([
  195. f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("灵感点", ""))}</span></div>'
  196. for p in inspiration_points[:3] # 最多显示3个
  197. ])
  198. points_sections.append(f'<div class="points-section"><div class="points-label">灵感点</div><div class="points-list">{insp_items}</div></div>')
  199. # 关键点
  200. if key_points:
  201. key_items = "".join([
  202. f'<div class="point-item"><span class="point-name">{html_module.escape(k.get("关键点", ""))}</span></div>'
  203. for k in key_points[:3] # 最多显示3个
  204. ])
  205. points_sections.append(f'<div class="points-section"><div class="points-label">关键点</div><div class="points-list">{key_items}</div></div>')
  206. # 目的点
  207. if purpose_points:
  208. purpose_items = "".join([
  209. f'<div class="point-item"><span class="point-name">{html_module.escape(p.get("目的点", ""))}</span></div>'
  210. for p in purpose_points[:3] # 最多显示3个
  211. ])
  212. points_sections.append(f'<div class="points-section"><div class="points-label">目的点</div><div class="points-list">{purpose_items}</div></div>')
  213. if points_sections:
  214. points_html = f'<div class="note-points">{"".join(points_sections)}</div>'
  215. # 生成发布日期HTML
  216. publish_date_html = ""
  217. if publish_time:
  218. publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
  219. card_html = f'''
  220. <div class="search-note-item" data-note-data='{note_data_json_escaped}' onclick="showNoteDetail(this)">
  221. {images_html}
  222. <div class="note-content">
  223. <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
  224. <div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
  225. {publish_date_html}
  226. <div class="note-footer">
  227. <div class="note-author">@{html_module.escape(author) if author else "匿名"}</div>
  228. <div class="note-stats">
  229. <span>👍 {like_count if like_count else 0}</span>
  230. {f'<span>💬 {comment_count}</span>' if comment_count else ''}
  231. </div>
  232. </div>
  233. </div>
  234. {f'<div class="note-points-hover">{points_html}</div>' if points_html else ''}
  235. </div>
  236. '''
  237. return card_html
  238. def generate_inspiration_card_html(
  239. inspiration_data: Dict[str, Any],
  240. inspiration_to_post_data: Dict[str, Any] = None,
  241. category_index_data: Dict[str, Any] = None,
  242. post_to_mapping_data: Dict[str, Any] = None
  243. ) -> str:
  244. """
  245. 生成单个灵感点的卡片HTML
  246. Args:
  247. inspiration_data: 灵感点数据
  248. inspiration_to_post_data: 点到帖子映射数据
  249. category_index_data: 分类索引数据
  250. Returns:
  251. HTML字符串
  252. """
  253. summary = inspiration_data.get("summary", {})
  254. step1 = inspiration_data.get("step1", {})
  255. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  256. search_results = inspiration_data.get("search_results", {})
  257. # 提取关键指标
  258. metrics = summary.get("关键指标", {})
  259. step1_score = metrics.get("step1_top1_score", 0)
  260. step1_match_element = metrics.get("step1_top1_匹配要素", "")
  261. # 确定卡片颜色(基于Step1分数)
  262. if step1_score >= 0.7:
  263. border_color = "#10b981"
  264. step1_color = "#10b981"
  265. elif step1_score >= 0.5:
  266. border_color = "#f59e0b"
  267. step1_color = "#f59e0b"
  268. elif step1_score >= 0.3:
  269. border_color = "#3b82f6"
  270. step1_color = "#3b82f6"
  271. else:
  272. border_color = "#ef4444"
  273. step1_color = "#ef4444"
  274. # 转义HTML
  275. inspiration_name_escaped = html_module.escape(inspiration_name)
  276. step1_match_element_escaped = html_module.escape(step1_match_element)
  277. # 获取Step1匹配结果(简要展示)
  278. step1_matches = step1.get("匹配结果列表", []) if step1 else []
  279. step1_match_preview = ""
  280. if step1_matches:
  281. top_match = step1_matches[0]
  282. # 从新的数据结构中提取信息
  283. input_info = top_match.get("输入信息", {})
  284. match_result = top_match.get("匹配结果", {})
  285. element_name = input_info.get("A", "")
  286. match_score = match_result.get("score", 0)
  287. same_parts = match_result.get("相同部分", {}) or {}
  288. increment_parts = match_result.get("增量部分", {}) or {}
  289. # 生成相同部分和增量部分的HTML
  290. parts_html = ""
  291. if same_parts:
  292. same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()]
  293. parts_html += f'<div class="preview-parts same"><strong>相同:</strong> {", ".join(same_items)}</div>'
  294. if increment_parts:
  295. inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()]
  296. parts_html += f'<div class="preview-parts increment"><strong>增量:</strong> {", ".join(inc_items)}</div>'
  297. step1_match_preview = f'''
  298. <div class="match-preview">
  299. <div class="match-preview-header">🎯 Step1 Top1匹配</div>
  300. <div class="match-preview-content">
  301. <span class="match-preview-name">{html_module.escape(element_name)}</span>
  302. <span class="match-preview-score" style="color: {step1_color};">{match_score:.2f}</span>
  303. </div>
  304. {parts_html}
  305. </div>
  306. '''
  307. # 提取 top3 匹配信息
  308. top3_matches = []
  309. if step1:
  310. matches = step1.get("匹配结果列表", [])
  311. for i, match in enumerate(matches[:3]):
  312. input_info = match.get("输入信息", {})
  313. match_result = match.get("匹配结果", {})
  314. element_name = input_info.get("A", "")
  315. match_score = match_result.get("score", 0)
  316. context = input_info.get("A_Context", "")
  317. # 解析层级关系
  318. hierarchy = []
  319. if context:
  320. lines = context.split("\n")
  321. for line in lines:
  322. if ":" in line:
  323. key, value = line.split(":", 1)
  324. key = key.strip()
  325. value = value.strip()
  326. if key in ["所属视角", "一级分类", "二级分类"]:
  327. hierarchy.append(value)
  328. top3_matches.append({
  329. "rank": i + 1,
  330. "name": element_name,
  331. "score": match_score,
  332. "hierarchy": hierarchy
  333. })
  334. # 准备详细数据用于弹窗
  335. detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
  336. detail_data_json_escaped = html_module.escape(detail_data_json)
  337. # top3 匹配数据
  338. top3_json = json.dumps(top3_matches, ensure_ascii=False)
  339. top3_json_escaped = html_module.escape(top3_json)
  340. # 生成详细HTML并进行HTML转义
  341. detail_html = generate_detail_html(inspiration_data)
  342. detail_html_escaped = html_module.escape(detail_html)
  343. # 生成匹配列表HTML
  344. matches_html = ""
  345. if step1:
  346. step1_matches = step1.get("匹配结果列表", [])
  347. for idx, match in enumerate(step1_matches):
  348. input_info = match.get("输入信息", {})
  349. match_result = match.get("匹配结果", {})
  350. element_name = input_info.get("A", "")
  351. context = input_info.get("A_Context", "")
  352. score = match_result.get("score", 0)
  353. score_explain = match_result.get("score说明", "") or ""
  354. same_parts = match_result.get("相同部分", {}) or {}
  355. increment_parts = match_result.get("增量部分", {}) or {}
  356. # 为搜索结果容器生成唯一ID
  357. safe_insp_name = ''.join(c if c.isalnum() else '_' for c in inspiration_name)
  358. unique_match_id = f"{safe_insp_name}-match-{idx}"
  359. # 解析层级
  360. hierarchy = []
  361. if context:
  362. lines = context.split("\n")
  363. for line in lines:
  364. if ":" in line:
  365. key, value = line.split(":", 1)
  366. key = key.strip()
  367. value = value.strip()
  368. if key in ["所属视角", "一级分类", "二级分类"]:
  369. hierarchy.append(value)
  370. hierarchy_html = " › ".join([html_module.escape(h) for h in hierarchy]) if hierarchy else ""
  371. rank_class = f"rank-{idx + 1}" if idx < 3 else ""
  372. # 相同部分HTML
  373. same_parts_html = ""
  374. if same_parts:
  375. same_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in same_parts.items()])
  376. same_parts_html = f'''
  377. <div class="match-parts same-parts">
  378. <div class="match-parts-title">✅ 相同部分</div>
  379. {same_items}
  380. </div>
  381. '''
  382. # 增量部分HTML
  383. increment_parts_html = ""
  384. if increment_parts:
  385. inc_items = "".join([f'<div class="part-item"><span class="part-key">{html_module.escape(k)}:</span><span class="part-value">{html_module.escape(v)}</span></div>' for k, v in increment_parts.items()])
  386. increment_parts_html = f'''
  387. <div class="match-parts increment-parts">
  388. <div class="match-parts-title">➕ 增量部分</div>
  389. {inc_items}
  390. </div>
  391. '''
  392. # 生成搜索结果HTML(网格展示,图片轮播)
  393. search_html = ""
  394. if element_name in search_results:
  395. result_obj = search_results[element_name]
  396. search_data = result_obj.get("search_data", {})
  397. match_data = result_obj.get("match_data", None)
  398. search_params = search_data.get("search_params", {})
  399. notes = search_data.get("notes", [])
  400. notes_count = len(notes)
  401. # 构建匹配分数字典 {channel_content_id: match_info}
  402. match_scores = {}
  403. if match_data and "匹配结果列表" in match_data:
  404. for match_item in match_data["匹配结果列表"]:
  405. business_info = match_item.get("业务信息", {})
  406. input_info = match_item.get("输入信息", {})
  407. content_id = business_info.get("channel_content_id", "")
  408. if content_id:
  409. match_scores[content_id] = {
  410. "score": match_item.get("匹配结果", {}).get("score", 0),
  411. "score说明": match_item.get("匹配结果", {}).get("score说明", "") or "",
  412. "相同部分": match_item.get("匹配结果", {}).get("相同部分", {}) or {},
  413. "增量部分": match_item.get("匹配结果", {}).get("增量部分", {}) or {},
  414. "输入B": input_info.get("B", "") or "",
  415. "输入A": input_info.get("A", "") or "",
  416. "B_Context": input_info.get("B_Context", "") or "",
  417. "A_Context": input_info.get("A_Context", "") or ""
  418. }
  419. # 为notes添加匹配分数、原始索引,并准备排序
  420. notes_with_scores = []
  421. for original_idx, note in enumerate(notes):
  422. note_id = note.get("channel_content_id", "")
  423. score_info = match_scores.get(note_id, None)
  424. notes_with_scores.append({
  425. "note": note,
  426. "score_info": score_info,
  427. "original_index": original_idx, # 原始搜索结果位置(0-based)
  428. "page": (original_idx // 20) + 1, # 第几页(假设每页20条)
  429. "position_in_page": (original_idx % 20) + 1 # 页内位置
  430. })
  431. # 默认按分数降序排序(没有分数的放到最后)
  432. notes_with_scores.sort(key=lambda x: x["score_info"]["score"] if x["score_info"] else -1, reverse=True)
  433. # 生成搜索参数HTML
  434. search_params_html = ""
  435. if search_params:
  436. keyword = search_params.get("keyword", "")
  437. content_type = search_params.get("content_type", "不限")
  438. sort_type = search_params.get("sort_type", "综合")
  439. publish_time = search_params.get("publish_time", "不限")
  440. search_params_html = f'''
  441. <div class="search-params-section">
  442. <div class="search-params-title">🔍 搜索参数</div>
  443. <div class="search-params-grid">
  444. <div class="search-param-item">
  445. <span class="search-param-label">关键词:</span>
  446. <span class="search-param-value">{html_module.escape(keyword)}</span>
  447. </div>
  448. <div class="search-param-item">
  449. <span class="search-param-label">内容类型:</span>
  450. <span class="search-param-value">{html_module.escape(content_type)}</span>
  451. </div>
  452. <div class="search-param-item">
  453. <span class="search-param-label">排序方式:</span>
  454. <span class="search-param-value">{html_module.escape(sort_type)}</span>
  455. </div>
  456. <div class="search-param-item">
  457. <span class="search-param-label">发布时间:</span>
  458. <span class="search-param-value">{html_module.escape(publish_time)}</span>
  459. </div>
  460. </div>
  461. </div>
  462. '''
  463. # 生成搜索结果统计HTML(带排序按钮)
  464. search_summary_html = f'''
  465. <div class="search-summary-section">
  466. <div class="search-summary-header">
  467. <div class="search-summary-left">
  468. <div class="search-summary-title">📊 搜索结果</div>
  469. <div class="search-summary-content">
  470. 共找到 <span class="search-result-count">{notes_count}</span> 条相关内容
  471. </div>
  472. </div>
  473. <div class="search-sort-buttons">
  474. <span class="search-sort-label">排序:</span>
  475. <button class="search-sort-btn active" data-sort="score" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">匹配分数</button>
  476. <button class="search-sort-btn" data-sort="original" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">原始顺序</button>
  477. <button class="search-sort-btn" data-sort="likes" onclick="sortSearchResults(this, '{unique_match_id}-search-results')">点赞数</button>
  478. </div>
  479. </div>
  480. </div>
  481. '''
  482. if notes_count > 0:
  483. notes_items = ""
  484. for note_idx, item in enumerate(notes_with_scores): # 使用包含元数据的列表
  485. note = item["note"]
  486. score_info = item["score_info"]
  487. original_index = item["original_index"]
  488. page = item["page"]
  489. position_in_page = item["position_in_page"]
  490. title = note.get("title", "")
  491. desc = note.get("desc", "")
  492. link = note.get("link", "")
  493. author = note.get("channel_account_name", "")
  494. like_count = note.get("like_count", 0)
  495. comment_count = note.get("comment_count", 0)
  496. images = note.get("images", [])
  497. content_id = note.get("channel_content_id", "")
  498. publish_time = note.get("publish_time", "")
  499. note_id = f"note-{idx}-{note_idx}"
  500. # 生成图片轮播HTML
  501. images_html = ""
  502. if images and len(images) > 0:
  503. images_track = "".join([f'<img src="{img}" class="note-image" alt="图片{i+1}">' for i, img in enumerate(images)])
  504. # 图片导航按钮和指示点
  505. nav_buttons = ""
  506. dots_html = ""
  507. if len(images) > 1:
  508. nav_buttons = f'''
  509. <button class="note-carousel-button prev" onclick="event.stopPropagation(); moveNoteImage('{note_id}', -1)">‹</button>
  510. <button class="note-carousel-button next" onclick="event.stopPropagation(); moveNoteImage('{note_id}', 1)">›</button>
  511. '''
  512. dots = "".join([f'<div class="note-image-dot{" active" if i == 0 else ""}"></div>' for i in range(len(images))])
  513. dots_html = f'<div class="note-image-dots" id="{note_id}-dots">{dots}</div>'
  514. images_html = f'''
  515. <div class="note-image-carousel" data-note-id="{note_id}" data-total-images="{len(images)}">
  516. <div class="note-images-track" id="{note_id}-track">
  517. {images_track}
  518. </div>
  519. {nav_buttons}
  520. {dots_html}
  521. </div>
  522. '''
  523. else:
  524. # 无图片时显示占位符
  525. images_html = f'''
  526. <div class="note-image-carousel">
  527. <div class="note-images-track">
  528. <div class="note-image" style="display: flex; align-items: center; justify-content: center; background: #f3f4f6; color: #9ca3af;">
  529. 暂无图片
  530. </div>
  531. </div>
  532. </div>
  533. '''
  534. # 准备详情数据
  535. note_data = {
  536. "title": title,
  537. "desc": desc,
  538. "link": link,
  539. "author": author,
  540. "like_count": like_count,
  541. "comment_count": comment_count,
  542. "images": images
  543. }
  544. note_data_json = json.dumps(note_data, ensure_ascii=False)
  545. note_data_escaped = html_module.escape(note_data_json)
  546. # 生成匹配分数HTML
  547. score_badge_html = ""
  548. score_detail_html = ""
  549. if score_info:
  550. note_score = score_info["score"]
  551. note_score_explain = score_info.get("score说明", "") or ""
  552. note_same_parts = score_info.get("相同部分", {}) or {}
  553. note_increment_parts = score_info.get("增量部分", {}) or {}
  554. input_b = score_info.get("输入B", "") or ""
  555. input_a = score_info.get("输入A", "") or ""
  556. b_context = score_info.get("B_Context", "") or ""
  557. a_context = score_info.get("A_Context", "") or ""
  558. # 分数详情JSON
  559. score_detail_data = {
  560. "score": note_score,
  561. "score说明": note_score_explain,
  562. "相同部分": note_same_parts,
  563. "增量部分": note_increment_parts,
  564. "输入B": input_b,
  565. "输入A": input_a,
  566. "B_Context": b_context,
  567. "A_Context": a_context
  568. }
  569. score_detail_json = json.dumps(score_detail_data, ensure_ascii=False)
  570. score_detail_escaped = html_module.escape(score_detail_json)
  571. score_badge_html = f'''
  572. <div class="note-score-badge" onclick="event.stopPropagation(); showScoreDetail(this)" data-score-detail='{score_detail_escaped}'>
  573. <span class="score-label">匹配分数</span>
  574. <span class="score-value">{note_score:.2f}</span>
  575. </div>
  576. '''
  577. # 生成发布日期HTML
  578. publish_date_html = ""
  579. if publish_time:
  580. publish_date_html = f'<div class="note-publish-date">📅 {html_module.escape(publish_time)}</div>'
  581. # 计算匹配分数(用于排序)
  582. sort_score = score_info["score"] if score_info else -1
  583. notes_items += f'''
  584. <div class="search-note-item"
  585. data-note-data='{note_data_escaped}'
  586. data-original-index="{original_index}"
  587. data-score="{sort_score}"
  588. data-likes="{like_count}"
  589. onclick="showNoteDetail(this)">
  590. {score_badge_html}
  591. {images_html}
  592. <div class="note-content">
  593. <div class="note-title">{html_module.escape(title) if title else "无标题"}</div>
  594. <div class="note-desc">{html_module.escape(desc) if desc else "暂无描述"}</div>
  595. {publish_date_html}
  596. <div class="note-footer">
  597. <div class="note-author">@{html_module.escape(author)}</div>
  598. <div class="note-position">P{page}-{position_in_page}</div>
  599. <div class="note-stats">
  600. <span>👍 {like_count}</span>
  601. <span>💬 {comment_count}</span>
  602. </div>
  603. </div>
  604. </div>
  605. </div>
  606. '''
  607. search_html = f'''
  608. <div class="search-results-section">
  609. {search_params_html}
  610. {search_summary_html}
  611. <div class="search-notes-list" id="{unique_match_id}-search-results">
  612. {notes_items}
  613. </div>
  614. </div>
  615. '''
  616. else:
  617. # 没有搜索结果时也显示参数
  618. search_html = f'''
  619. <div class="search-results-section">
  620. {search_params_html}
  621. {search_summary_html}
  622. </div>
  623. '''
  624. # 只有第一个匹配项默认展开
  625. expanded_class = " expanded" if idx == 0 else ""
  626. # 生成当前匹配项的灵感点和灵感分类详情
  627. match_info_section = ""
  628. if inspiration_to_post_data and category_index_data:
  629. # 获取灵感点数据
  630. inspiration_points = inspiration_to_post_data.get("点到帖子映射", {}).get("灵感点", {})
  631. inspiration_info = inspiration_points.get(inspiration_name, {})
  632. # 获取当前匹配的灵感分类数据
  633. categories = category_index_data.get("灵感分类", {})
  634. category_info = categories.get(element_name, {})
  635. # 生成灵感点详情HTML(左栏)
  636. insp_detail_html = ""
  637. if inspiration_info:
  638. insp_dimension = inspiration_info.get("维度", "")
  639. insp_desc = inspiration_info.get("描述", "")
  640. insp_posts = inspiration_info.get("帖子详情列表", [])
  641. # 生成帖子卡片
  642. insp_posts_html = ""
  643. for post in insp_posts[:6]: # 最多显示6个
  644. insp_posts_html += generate_post_card_html(post, f"{unique_match_id}-insp-post", post_to_mapping_data)
  645. insp_detail_html = f'''
  646. <div class="info-detail-column">
  647. <div class="info-header">
  648. <span class="info-label">[灵感点]</span>
  649. <span class="info-name">{html_module.escape(inspiration_name)}</span>
  650. </div>
  651. <div class="info-content">
  652. {f'<div class="info-field"><span class="info-field-label">维度:</span><span class="info-field-value">{html_module.escape(insp_dimension)}</span></div>' if insp_dimension else ''}
  653. {f'<div class="info-field"><span class="info-field-label">描述:</span><span class="info-field-value">{html_module.escape(insp_desc)}</span></div>' if insp_desc else ''}
  654. {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{insp_posts_html}</div></div>' if insp_posts_html else ''}
  655. </div>
  656. </div>
  657. '''
  658. # 生成灵感分类详情HTML(右栏)
  659. cat_detail_html = ""
  660. if category_info:
  661. cat_level = category_info.get("分类层级", "")
  662. cat_definition = category_info.get("分类定义", "")
  663. cat_posts = category_info.get("帖子详情列表", [])
  664. # 生成帖子卡片
  665. cat_posts_html = ""
  666. for post in cat_posts[:6]: # 最多显示6个
  667. cat_posts_html += generate_post_card_html(post, f"{unique_match_id}-cat-post", post_to_mapping_data)
  668. cat_detail_html = f'''
  669. <div class="info-detail-column">
  670. <div class="info-header">
  671. <span class="info-label">[灵感分类]</span>
  672. <span class="info-name">{html_module.escape(element_name)}</span>
  673. </div>
  674. <div class="info-content">
  675. {f'<div class="info-field"><span class="info-field-label">层级:</span><span class="info-field-value">{html_module.escape(cat_level)}</span></div>' if cat_level else ''}
  676. {f'<div class="info-field"><span class="info-field-label">定义:</span><span class="info-field-value">{html_module.escape(cat_definition)}</span></div>' if cat_definition else ''}
  677. {f'<div class="info-posts"><div class="info-posts-title">相关帖子</div><div class="info-posts-grid">{cat_posts_html}</div></div>' if cat_posts_html else ''}
  678. </div>
  679. </div>
  680. '''
  681. if insp_detail_html or cat_detail_html:
  682. match_info_section = f'''
  683. <div class="inspiration-category-section">
  684. {insp_detail_html}
  685. {cat_detail_html}
  686. </div>
  687. '''
  688. # 步骤1:灵感点匹配灵感分类
  689. step1_html = f'''
  690. <div class="step-section-wrapper step-1-wrapper expanded">
  691. <div class="step-header" onclick="toggleStepWrapper(this)">
  692. <div class="step-header-content">
  693. <span class="step-number-badge">步骤 1</span>
  694. <span class="step-title">灵感点匹配灵感分类</span>
  695. </div>
  696. <div class="step-toggle">▼</div>
  697. </div>
  698. <div class="step-wrapper-content">
  699. <div class="match-analysis-section" id="{unique_match_id}-step1" data-step-name="灵感点匹配灵感分类">
  700. <div class="match-parts-container">
  701. <div class="match-parts-column">
  702. {same_parts_html}
  703. </div>
  704. <div class="match-parts-column">
  705. {increment_parts_html}
  706. </div>
  707. </div>
  708. {f'<div class="match-explain"><div class="match-explain-title">💡 分数说明</div><div class="match-explain-text">{html_module.escape(score_explain)}</div></div>' if score_explain else ''}
  709. </div>
  710. </div>
  711. </div>
  712. '''
  713. # 步骤2:搜索结果(如果有)
  714. step2_html = ""
  715. if search_html:
  716. step2_html = f'''
  717. <div class="step-section-wrapper step-2-wrapper expanded">
  718. <div class="step-header" onclick="toggleStepWrapper(this)">
  719. <div class="step-header-content">
  720. <span class="step-number-badge">步骤 2</span>
  721. <span class="step-title">搜索</span>
  722. </div>
  723. <div class="step-toggle">▼</div>
  724. </div>
  725. <div class="step-wrapper-content">
  726. <div class="step-section expanded" data-step="2" id="{unique_match_id}-step2" data-step-name="灵感分类搜索">
  727. <div class="step-section-header" onclick="toggleStep(this)">
  728. <div class="step-section-title">
  729. <span class="step-sub-number">2.1</span>
  730. <span>直接搜索灵感分类</span>
  731. </div>
  732. <div class="step-toggle">▼</div>
  733. </div>
  734. <div class="step-section-content">
  735. {search_html}
  736. </div>
  737. </div>
  738. </div>
  739. </div>
  740. '''
  741. # 创建安全的ID(移除特殊字符)
  742. safe_element_id = ''.join(c if c.isalnum() or c in '_-' else '_' for c in element_name)
  743. matches_html += f'''
  744. <div class="match-item{expanded_class}" data-index="{idx}" id="{unique_match_id}" data-match-name="{html_module.escape(element_name)}">
  745. <div class="match-main-header" onclick="toggleMainMatch(this)">
  746. <div class="match-header-row">
  747. <div class="match-header-left">
  748. <span class="match-rank {rank_class}">Top {idx + 1}</span>
  749. <span class="detail-label">[灵感点]</span>
  750. <span class="match-title">{html_module.escape(inspiration_name)}</span>
  751. </div>
  752. <div class="match-header-center">
  753. <span class="match-score-label">匹配分数</span>
  754. <span class="match-score-value">{score:.2f}</span>
  755. </div>
  756. <div class="match-header-right">
  757. <span class="detail-label">[灵感分类]</span>
  758. <span class="match-category">{html_module.escape(element_name)}</span>
  759. <span class="match-hierarchy">({hierarchy_html})</span>
  760. </div>
  761. </div>
  762. <div class="match-toggle-main">▼</div>
  763. </div>
  764. <div class="match-main-content">
  765. {match_info_section}
  766. {step1_html}
  767. {step2_html}
  768. </div>
  769. </div>
  770. '''
  771. # 获取top1匹配的灵感分类名称(用于顶部标题)
  772. top1_category_name = ""
  773. if step1:
  774. step1_matches = step1.get("匹配结果列表", [])
  775. if step1_matches:
  776. top_match = step1_matches[0]
  777. input_info = top_match.get("输入信息", {})
  778. top1_category_name = input_info.get("A", "")
  779. html = f'''
  780. <div class="inspiration-detail"
  781. data-inspiration-name="{inspiration_name_escaped}"
  782. data-step1-score="{step1_score}"
  783. data-top3-matches="{top3_json_escaped}">
  784. <div class="breadcrumb-container">
  785. <div class="breadcrumb" id="dynamicBreadcrumb">
  786. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> {inspiration_name_escaped}</span>
  787. </div>
  788. </div>
  789. <div class="inspiration-content-wrapper">
  790. <div class="matches-list">
  791. {matches_html}
  792. </div>
  793. </div>
  794. </div>
  795. '''
  796. return html
  797. def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
  798. """
  799. 生成灵感点的详细信息HTML
  800. Args:
  801. inspiration_data: 灵感点数据
  802. Returns:
  803. 详细信息的HTML字符串
  804. """
  805. import html as html_module
  806. summary = inspiration_data.get("summary", {})
  807. step1 = inspiration_data.get("step1", {})
  808. step2 = inspiration_data.get("step2", {})
  809. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  810. content = f'''
  811. <div class="modal-header">
  812. <h2 class="modal-title">{html_module.escape(inspiration_name)}</h2>
  813. </div>
  814. '''
  815. # 获取元数据,用于后面的日志链接
  816. metadata = summary.get("元数据", {})
  817. # Step1 详细信息
  818. if step1 and step1.get("灵感"):
  819. inspiration = step1.get("灵感", "")
  820. matches = step1.get("匹配结果列表", [])
  821. content += f'''
  822. <div class="modal-section">
  823. <h3>🎯 Step1: 灵感人设匹配</h3>
  824. <div class="step-content">
  825. <div class="step-field">
  826. <span class="step-field-label">灵感内容:</span>
  827. <span class="step-field-value">{html_module.escape(inspiration)}</span>
  828. </div>
  829. '''
  830. # 显示匹配结果(只显示Top1)
  831. if matches:
  832. content += f'''
  833. <div class="step-field">
  834. <span class="step-field-label">Top1匹配结果:</span>
  835. <div class="matches-list">
  836. '''
  837. for index, match in enumerate(matches[:1]):
  838. input_info = match.get("输入信息", {})
  839. match_result = match.get("匹配结果", {})
  840. element_a = input_info.get("A", "")
  841. context_a = input_info.get("A_Context", "")
  842. score = match_result.get("score", 0)
  843. score_explain = match_result.get("score说明", "") or ""
  844. same_parts = match_result.get("相同部分", {}) or {}
  845. increment_parts = match_result.get("增量部分", {}) or {}
  846. content += f'''
  847. <div class="match-item">
  848. <div class="match-header">
  849. <span class="match-element-name">{html_module.escape(element_a)}</span>
  850. <span class="match-score">{score:.2f}</span>
  851. </div>
  852. '''
  853. if context_a:
  854. content += f'<div class="match-context"><strong>📍 所属分类:</strong> {html_module.escape(context_a).replace(chr(10), "<br>")}</div>'
  855. if score_explain:
  856. content += f'<div class="match-explain"><strong>💡 分数说明:</strong> {html_module.escape(score_explain)}</div>'
  857. # 相同部分
  858. if same_parts:
  859. content += '''
  860. <div class="match-parts same-parts">
  861. <div class="parts-header">✅ 相同部分</div>
  862. <div class="parts-content">
  863. '''
  864. for key, value in same_parts.items():
  865. content += f'''
  866. <div class="part-item">
  867. <span class="part-key">{html_module.escape(key)}:</span>
  868. <span class="part-value">{html_module.escape(value)}</span>
  869. </div>
  870. '''
  871. content += '''
  872. </div>
  873. </div>
  874. '''
  875. # 增量部分
  876. if increment_parts:
  877. content += '''
  878. <div class="match-parts increment-parts">
  879. <div class="parts-header">➕ 增量部分</div>
  880. <div class="parts-content">
  881. '''
  882. for key, value in increment_parts.items():
  883. content += f'''
  884. <div class="part-item">
  885. <span class="part-key">{html_module.escape(key)}:</span>
  886. <span class="part-value">{html_module.escape(value)}</span>
  887. </div>
  888. '''
  889. content += '''
  890. </div>
  891. </div>
  892. '''
  893. content += '''
  894. </div>
  895. '''
  896. content += '''
  897. </div>
  898. </div>
  899. '''
  900. content += '''
  901. </div>
  902. </div>
  903. '''
  904. # 日志链接
  905. if metadata.get("log_url"):
  906. content += f'''
  907. <div class="modal-link">
  908. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  909. 🔗 查看详细日志
  910. </a>
  911. </div>
  912. '''
  913. return content
  914. def generate_detail_modal_content_js() -> str:
  915. """
  916. 生成详情弹窗内容的JavaScript函数
  917. Returns:
  918. JavaScript代码字符串
  919. """
  920. return '''
  921. // 笔记图片当前索引管理
  922. const noteImageStates = {};
  923. // 移动笔记图片
  924. function moveNoteImage(noteId, direction) {
  925. if (!noteImageStates[noteId]) {
  926. noteImageStates[noteId] = 0;
  927. }
  928. const carousel = document.querySelector(`[data-note-id="${noteId}"]`);
  929. if (!carousel) return;
  930. const totalImages = parseInt(carousel.dataset.totalImages);
  931. const track = document.getElementById(noteId + '-track');
  932. if (!track) return;
  933. let newIndex = noteImageStates[noteId] + direction;
  934. if (newIndex < 0) newIndex = 0;
  935. if (newIndex >= totalImages) newIndex = totalImages - 1;
  936. noteImageStates[noteId] = newIndex;
  937. // 移动轨道
  938. track.style.transform = `translateX(-${newIndex * 100}%)`;
  939. // 更新指示点
  940. const dots = document.querySelectorAll(`#${noteId}-dots .note-image-dot`);
  941. dots.forEach((dot, i) => {
  942. dot.classList.toggle('active', i === newIndex);
  943. });
  944. // 更新按钮状态
  945. const prevBtn = carousel.querySelector('.note-carousel-button.prev');
  946. const nextBtn = carousel.querySelector('.note-carousel-button.next');
  947. if (prevBtn) {
  948. prevBtn.classList.toggle('disabled', newIndex === 0);
  949. }
  950. if (nextBtn) {
  951. nextBtn.classList.toggle('disabled', newIndex >= totalImages - 1);
  952. }
  953. }
  954. // 显示笔记详情
  955. function showNoteDetail(element) {
  956. const noteDataStr = element.dataset.noteData;
  957. if (!noteDataStr) return;
  958. try {
  959. const noteData = JSON.parse(noteDataStr);
  960. // 生成图片HTML
  961. let imagesHtml = '';
  962. if (noteData.images && noteData.images.length > 0) {
  963. imagesHtml = noteData.images.map(img =>
  964. `<img src="${img}" class="note-detail-image" alt="图片">`
  965. ).join('');
  966. } else {
  967. imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
  968. }
  969. // 生成灵感点、关键点、目的点HTML
  970. let pointsDetailHtml = '';
  971. const hasPoints = (noteData.inspiration_points && noteData.inspiration_points.length > 0) ||
  972. (noteData.key_points && noteData.key_points.length > 0) ||
  973. (noteData.purpose_points && noteData.purpose_points.length > 0);
  974. if (hasPoints) {
  975. let sections = [];
  976. // 灵感点
  977. if (noteData.inspiration_points && noteData.inspiration_points.length > 0) {
  978. const items = noteData.inspiration_points.map(p =>
  979. `<div class="detail-point-item">
  980. <div class="detail-point-name">${p.灵感点 || ''}</div>
  981. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  982. </div>`
  983. ).join('');
  984. sections.push(`<div class="detail-points-section">
  985. <div class="detail-points-title">💡 灵感点</div>
  986. ${items}
  987. </div>`);
  988. }
  989. // 关键点
  990. if (noteData.key_points && noteData.key_points.length > 0) {
  991. const items = noteData.key_points.map(k =>
  992. `<div class="detail-point-item">
  993. <div class="detail-point-name">${k.关键点 || ''}</div>
  994. ${k.描述 ? `<div class="detail-point-desc">${k.描述}</div>` : ''}
  995. </div>`
  996. ).join('');
  997. sections.push(`<div class="detail-points-section">
  998. <div class="detail-points-title">🔑 关键点</div>
  999. ${items}
  1000. </div>`);
  1001. }
  1002. // 目的点
  1003. if (noteData.purpose_points && noteData.purpose_points.length > 0) {
  1004. const items = noteData.purpose_points.map(p =>
  1005. `<div class="detail-point-item">
  1006. <div class="detail-point-name">${p.目的点 || ''}</div>
  1007. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  1008. </div>`
  1009. ).join('');
  1010. sections.push(`<div class="detail-points-section">
  1011. <div class="detail-points-title">🎯 目的点</div>
  1012. ${items}
  1013. </div>`);
  1014. }
  1015. pointsDetailHtml = `<div class="note-detail-points">${sections.join('')}</div>`;
  1016. }
  1017. const modalHtml = `
  1018. <div class="note-detail-content">
  1019. <button class="note-detail-close" onclick="closeNoteDetail()">×</button>
  1020. <div class="note-detail-header">
  1021. <div class="note-detail-title">${noteData.title || '无标题'}</div>
  1022. <div class="note-detail-meta">
  1023. <span class="note-detail-author">@${noteData.author}</span>
  1024. <div class="note-detail-stats">
  1025. <span>👍 ${noteData.like_count}</span>
  1026. <span>💬 ${noteData.comment_count}</span>
  1027. </div>
  1028. </div>
  1029. </div>
  1030. <div class="note-detail-body">
  1031. ${noteData.desc ? `<div class="note-detail-desc">${noteData.desc}</div>` : ''}
  1032. ${pointsDetailHtml}
  1033. <div class="note-detail-images">
  1034. ${imagesHtml}
  1035. </div>
  1036. </div>
  1037. <div class="note-detail-footer">
  1038. <a href="${noteData.link}" target="_blank" class="note-detail-link">
  1039. 在小红书查看完整内容 →
  1040. </a>
  1041. </div>
  1042. </div>
  1043. `;
  1044. let modal = document.getElementById('noteDetailModal');
  1045. if (!modal) {
  1046. modal = document.createElement('div');
  1047. modal.id = 'noteDetailModal';
  1048. modal.className = 'note-detail-modal';
  1049. modal.onclick = (e) => {
  1050. if (e.target === modal) closeNoteDetail();
  1051. };
  1052. document.body.appendChild(modal);
  1053. }
  1054. modal.innerHTML = modalHtml;
  1055. modal.classList.add('active');
  1056. document.body.style.overflow = 'hidden';
  1057. } catch (e) {
  1058. console.error('Error parsing note data:', e);
  1059. }
  1060. }
  1061. // 关闭笔记详情
  1062. function closeNoteDetail() {
  1063. const modal = document.getElementById('noteDetailModal');
  1064. if (modal) {
  1065. modal.classList.remove('active');
  1066. document.body.style.overflow = '';
  1067. }
  1068. }
  1069. // 显示分数详情
  1070. function showScoreDetail(element) {
  1071. const scoreDetailStr = element.dataset.scoreDetail;
  1072. if (!scoreDetailStr) return;
  1073. try {
  1074. const scoreData = JSON.parse(scoreDetailStr);
  1075. const modal = document.getElementById('scoreDetailModal');
  1076. const modalBody = document.getElementById('scoreModalBody');
  1077. // 生成相同部分HTML
  1078. let samePartsHTML = '';
  1079. if (scoreData.相同部分 && Object.keys(scoreData.相同部分).length > 0) {
  1080. const sameItems = Object.entries(scoreData.相同部分).map(([key, value]) =>
  1081. `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
  1082. ).join('');
  1083. samePartsHTML = `
  1084. <div class="score-parts same-parts">
  1085. <div class="score-parts-title">✅ 相同部分</div>
  1086. ${sameItems}
  1087. </div>
  1088. `;
  1089. }
  1090. // 生成增量部分HTML
  1091. let incrementPartsHTML = '';
  1092. if (scoreData.增量部分 && Object.keys(scoreData.增量部分).length > 0) {
  1093. const incItems = Object.entries(scoreData.增量部分).map(([key, value]) =>
  1094. `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
  1095. ).join('');
  1096. incrementPartsHTML = `
  1097. <div class="score-parts increment-parts">
  1098. <div class="score-parts-title">➕ 增量部分</div>
  1099. ${incItems}
  1100. </div>
  1101. `;
  1102. }
  1103. // 生成分数说明HTML
  1104. let explainHTML = '';
  1105. if (scoreData.score说明) {
  1106. explainHTML = `
  1107. <div class="score-detail-explain">
  1108. <div class="score-explain-title">💡 分数说明</div>
  1109. <div class="score-explain-text">${scoreData.score说明}</div>
  1110. </div>
  1111. `;
  1112. }
  1113. // 生成输入信息HTML
  1114. let inputInfoHTML = '';
  1115. if (scoreData.输入B || scoreData.输入A) {
  1116. inputInfoHTML = `
  1117. <div class="score-input-info">
  1118. <div class="score-input-title">📝 输入信息</div>
  1119. ${scoreData.输入B ? `
  1120. <div class="score-input-item">
  1121. <div class="score-input-label">输入B(灵感点):</div>
  1122. <div class="score-input-value">${scoreData.输入B}</div>
  1123. </div>
  1124. ` : ''}
  1125. ${scoreData.B_Context ? `
  1126. <div class="score-input-item">
  1127. <div class="score-input-label">B_Context:</div>
  1128. <div class="score-input-value">${scoreData.B_Context}</div>
  1129. </div>
  1130. ` : ''}
  1131. ${scoreData.输入A ? `
  1132. <div class="score-input-item">
  1133. <div class="score-input-label">输入A(帖子标题):</div>
  1134. <div class="score-input-value">${scoreData.输入A}</div>
  1135. </div>
  1136. ` : ''}
  1137. ${scoreData.A_Context ? `
  1138. <div class="score-input-item">
  1139. <div class="score-input-label">A_Context(帖子内容摘要):</div>
  1140. <div class="score-input-value">${scoreData.A_Context}</div>
  1141. </div>
  1142. ` : ''}
  1143. </div>
  1144. `;
  1145. }
  1146. modalBody.innerHTML = `
  1147. <div class="score-detail-container">
  1148. <h2 class="score-detail-title">匹配分数详情</h2>
  1149. <div class="score-detail-score">
  1150. <span class="score-detail-label">匹配分数:</span>
  1151. <span class="score-detail-value">${scoreData.score.toFixed(2)}</span>
  1152. </div>
  1153. ${inputInfoHTML}
  1154. ${explainHTML}
  1155. <div class="score-parts-container">
  1156. ${samePartsHTML}
  1157. ${incrementPartsHTML}
  1158. </div>
  1159. </div>
  1160. `;
  1161. modal.classList.add('active');
  1162. document.body.style.overflow = 'hidden';
  1163. } catch (e) {
  1164. console.error('Failed to parse score detail:', e);
  1165. }
  1166. }
  1167. // 关闭分数详情
  1168. function closeScoreDetail() {
  1169. const modal = document.getElementById('scoreDetailModal');
  1170. if (modal) {
  1171. modal.classList.remove('active');
  1172. document.body.style.overflow = '';
  1173. }
  1174. }
  1175. // 点击Modal背景关闭分数详情
  1176. function closeScoreDetailModal(event) {
  1177. if (event.target.id === 'scoreDetailModal') {
  1178. closeScoreDetail();
  1179. }
  1180. }
  1181. // 搜索结果排序
  1182. function sortSearchResults(button, containerId) {
  1183. const sortType = button.dataset.sort;
  1184. const container = document.getElementById(containerId);
  1185. if (!container) return;
  1186. // 更新按钮状态
  1187. const allButtons = button.parentElement.querySelectorAll('.search-sort-btn');
  1188. allButtons.forEach(btn => btn.classList.remove('active'));
  1189. button.classList.add('active');
  1190. // 获取所有搜索结果卡片
  1191. const items = Array.from(container.querySelectorAll('.search-note-item'));
  1192. // 根据排序类型排序
  1193. items.sort((a, b) => {
  1194. if (sortType === 'score') {
  1195. const scoreA = parseFloat(a.dataset.score) || -1;
  1196. const scoreB = parseFloat(b.dataset.score) || -1;
  1197. return scoreB - scoreA; // 降序
  1198. } else if (sortType === 'original') {
  1199. const indexA = parseInt(a.dataset.originalIndex) || 0;
  1200. const indexB = parseInt(b.dataset.originalIndex) || 0;
  1201. return indexA - indexB; // 升序
  1202. } else if (sortType === 'likes') {
  1203. const likesA = parseInt(a.dataset.likes) || 0;
  1204. const likesB = parseInt(b.dataset.likes) || 0;
  1205. return likesB - likesA; // 降序
  1206. }
  1207. return 0;
  1208. });
  1209. // 重新排列DOM
  1210. items.forEach(item => container.appendChild(item));
  1211. }
  1212. // ESC键关闭详情
  1213. document.addEventListener('keydown', function(event) {
  1214. if (event.key === 'Escape') {
  1215. closeNoteDetail();
  1216. closeScoreDetail();
  1217. }
  1218. });
  1219. // 切换主匹配项的展开/折叠
  1220. function toggleMainMatch(element) {
  1221. const matchItem = element.closest('.match-item');
  1222. matchItem.classList.toggle('expanded');
  1223. }
  1224. // 切换步骤wrapper的展开/折叠
  1225. function toggleStepWrapper(element) {
  1226. const stepWrapper = element.closest('.step-section-wrapper');
  1227. stepWrapper.classList.toggle('expanded');
  1228. }
  1229. // 切换步骤的展开/折叠
  1230. function toggleStep(element) {
  1231. const stepSection = element.closest('.step-section');
  1232. stepSection.classList.toggle('expanded');
  1233. }
  1234. // 切换匹配详情的展开/折叠
  1235. function toggleMatchSection(element) {
  1236. const matchSection = element.closest('.match-section');
  1237. matchSection.classList.toggle('expanded');
  1238. }
  1239. // 显示指定的灵感详情
  1240. function showDetail(index) {
  1241. const details = document.querySelectorAll('.inspiration-detail');
  1242. details.forEach((detail, i) => {
  1243. if (i === index) {
  1244. detail.classList.add('active');
  1245. // 滚动到顶部
  1246. const section = document.querySelector('.inspirations-section');
  1247. if (section) {
  1248. section.scrollTop = 0;
  1249. }
  1250. } else {
  1251. detail.classList.remove('active');
  1252. }
  1253. });
  1254. }
  1255. // 生成导航目录
  1256. function generateNavigation() {
  1257. const details = document.querySelectorAll('.inspiration-detail');
  1258. const navList = document.getElementById('navList');
  1259. navList.innerHTML = '';
  1260. details.forEach((detail, index) => {
  1261. const name = detail.dataset.inspirationName;
  1262. const score = parseFloat(detail.dataset.step1Score) || 0;
  1263. const top3MatchesStr = detail.dataset.top3Matches;
  1264. let top3Matches = [];
  1265. try {
  1266. top3Matches = JSON.parse(top3MatchesStr);
  1267. } catch(e) {
  1268. console.error('Error parsing top3 matches:', e);
  1269. }
  1270. const navItem = document.createElement('div');
  1271. navItem.className = 'nav-item';
  1272. if (index === 0) navItem.classList.add('active');
  1273. navItem.dataset.cardIndex = index;
  1274. // 生成匹配列表HTML
  1275. let matchesHtml = '';
  1276. if (top3Matches && top3Matches.length > 0) {
  1277. matchesHtml = '<div class="nav-item-matches">';
  1278. top3Matches.forEach((match, i) => {
  1279. // 生成层级路径
  1280. let hierarchyHtml = '';
  1281. if (match.hierarchy && match.hierarchy.length > 0) {
  1282. hierarchyHtml = '<div class="nav-match-hierarchy">';
  1283. match.hierarchy.forEach((level, idx) => {
  1284. if (idx > 0) {
  1285. hierarchyHtml += '<span class="hierarchy-separator">›</span>';
  1286. }
  1287. hierarchyHtml += `<span class="hierarchy-item">${level}</span>`;
  1288. });
  1289. hierarchyHtml += '</div>';
  1290. }
  1291. matchesHtml += `
  1292. <div class="nav-match-item">
  1293. <span class="nav-match-rank rank-${match.rank}">Top${match.rank}</span>
  1294. <div class="nav-match-content">
  1295. <span class="nav-match-name">${match.name}</span>
  1296. ${hierarchyHtml}
  1297. <span class="nav-match-score">${match.score.toFixed(2)}</span>
  1298. </div>
  1299. </div>
  1300. `;
  1301. });
  1302. matchesHtml += '</div>';
  1303. }
  1304. navItem.innerHTML = `
  1305. <div class="nav-item-header">
  1306. <span class="nav-item-name" title="${name}">
  1307. <span class="nav-label">[灵感点]</span>${name}
  1308. </span>
  1309. <span class="nav-item-score">${score.toFixed(2)}</span>
  1310. </div>
  1311. ${matchesHtml}
  1312. `;
  1313. navItem.addEventListener('click', () => {
  1314. // 移除所有active状态
  1315. document.querySelectorAll('.nav-item').forEach(item => {
  1316. item.classList.remove('active');
  1317. });
  1318. // 添加当前active状态
  1319. navItem.classList.add('active');
  1320. // 显示对应详情
  1321. showDetail(index);
  1322. });
  1323. navList.appendChild(navItem);
  1324. });
  1325. // 默认显示第一个详情
  1326. if (details.length > 0) {
  1327. showDetail(0);
  1328. }
  1329. }
  1330. // 更新面包屑
  1331. function updateBreadcrumb(matchName, stepName) {
  1332. const breadcrumb = document.querySelector('.inspiration-detail.active #dynamicBreadcrumb');
  1333. if (!breadcrumb) return;
  1334. const inspirationName = document.querySelector('.inspiration-detail.active').dataset.inspirationName;
  1335. let breadcrumbHtml = `
  1336. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> ${inspirationName}</span>
  1337. `;
  1338. if (matchName) {
  1339. breadcrumbHtml += `
  1340. <span class="breadcrumb-separator">›</span>
  1341. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感分类]</span> ${matchName}</span>
  1342. `;
  1343. }
  1344. if (stepName) {
  1345. breadcrumbHtml += `
  1346. <span class="breadcrumb-separator">›</span>
  1347. <span class="breadcrumb-item breadcrumb-current">${stepName}</span>
  1348. `;
  1349. }
  1350. breadcrumb.innerHTML = breadcrumbHtml;
  1351. }
  1352. // 监听滚动,更新面包屑
  1353. function setupBreadcrumbObserver() {
  1354. const activeDetail = document.querySelector('.inspiration-detail.active');
  1355. if (!activeDetail) return;
  1356. const contentWrapper = activeDetail.querySelector('.inspiration-content-wrapper');
  1357. if (!contentWrapper) return;
  1358. // 获取所有需要监听的section
  1359. const sections = activeDetail.querySelectorAll('.step-section');
  1360. if (sections.length === 0) return;
  1361. // 创建Intersection Observer
  1362. const observerOptions = {
  1363. root: null,
  1364. rootMargin: '-100px 0px -50% 0px',
  1365. threshold: 0
  1366. };
  1367. const observer = new IntersectionObserver((entries) => {
  1368. entries.forEach(entry => {
  1369. if (entry.isIntersecting) {
  1370. const section = entry.target;
  1371. const matchItem = section.closest('.match-item');
  1372. const matchName = matchItem ? matchItem.dataset.matchName : '';
  1373. const stepName = section.dataset.stepName || '';
  1374. updateBreadcrumb(matchName, stepName);
  1375. }
  1376. });
  1377. }, observerOptions);
  1378. // 观察所有section
  1379. sections.forEach(section => observer.observe(section));
  1380. // 存储observer以便清理
  1381. if (!window.breadcrumbObservers) {
  1382. window.breadcrumbObservers = [];
  1383. }
  1384. window.breadcrumbObservers.push(observer);
  1385. }
  1386. // 页面加载时生成导航
  1387. document.addEventListener('DOMContentLoaded', () => {
  1388. generateNavigation();
  1389. setupBreadcrumbObserver();
  1390. });
  1391. // 当切换灵感点时,重新设置observer
  1392. const originalShowDetail = showDetail;
  1393. showDetail = function(index) {
  1394. // 清理旧的observers
  1395. if (window.breadcrumbObservers) {
  1396. window.breadcrumbObservers.forEach(obs => obs.disconnect());
  1397. window.breadcrumbObservers = [];
  1398. }
  1399. // 调用原始函数
  1400. originalShowDetail(index);
  1401. // 设置新的observer
  1402. setTimeout(() => {
  1403. setupBreadcrumbObserver();
  1404. }, 100);
  1405. };
  1406. '''
  1407. def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
  1408. """
  1409. 生成人设结构的树状HTML
  1410. Args:
  1411. persona_data: 人设数据
  1412. Returns:
  1413. 人设结构的HTML字符串
  1414. """
  1415. if not persona_data:
  1416. return '<div class="empty-state">暂无人设数据</div>'
  1417. inspiration_list = persona_data.get("灵感点列表", [])
  1418. if not inspiration_list:
  1419. return '<div class="empty-state">暂无灵感点列表数据</div>'
  1420. html_parts = ['<div class="tree">']
  1421. for perspective_idx, perspective in enumerate(inspiration_list):
  1422. perspective_name = perspective.get("视角名称", "未知视角")
  1423. perspective_desc = perspective.get("视角描述", "")
  1424. pattern_list = perspective.get("模式列表", [])
  1425. # 一级节点:视角
  1426. html_parts.append(f'''
  1427. <ul>
  1428. <li>
  1429. <div class="tree-node level-1">
  1430. <span class="node-icon">📁</span>
  1431. <span class="node-name">{html_module.escape(perspective_name)}</span>
  1432. <span class="node-count">{len(pattern_list)}个分类</span>
  1433. </div>
  1434. ''')
  1435. if perspective_desc:
  1436. html_parts.append(f'''
  1437. <div class="node-desc">{html_module.escape(perspective_desc)}</div>
  1438. ''')
  1439. # 二级节点:分类
  1440. if pattern_list:
  1441. html_parts.append('<ul>')
  1442. for pattern in pattern_list:
  1443. category_name = pattern.get("分类名称", "未知分类")
  1444. core_definition = pattern.get("核心定义", "")
  1445. subcategories = pattern.get("二级细分", [])
  1446. total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
  1447. html_parts.append(f'''
  1448. <li>
  1449. <div class="tree-node level-2">
  1450. <span class="node-icon">📂</span>
  1451. <span class="node-name">{html_module.escape(category_name)}</span>
  1452. <span class="node-count">{total_posts}个帖子</span>
  1453. </div>
  1454. ''')
  1455. if core_definition:
  1456. html_parts.append(f'''
  1457. <div class="node-desc">{html_module.escape(core_definition)}</div>
  1458. ''')
  1459. # 三级节点:细分
  1460. if subcategories:
  1461. html_parts.append('<ul>')
  1462. for subcategory in subcategories:
  1463. sub_name = subcategory.get("分类名称", "未知细分")
  1464. sub_definition = subcategory.get("分类定义", "")
  1465. post_ids = subcategory.get("帖子ID列表", [])
  1466. html_parts.append(f'''
  1467. <li>
  1468. <div class="tree-node level-3">
  1469. <span class="node-icon">📄</span>
  1470. <span class="node-name">{html_module.escape(sub_name)}</span>
  1471. <span class="node-count">{len(post_ids)}个帖子</span>
  1472. </div>
  1473. ''')
  1474. if sub_definition:
  1475. html_parts.append(f'''
  1476. <div class="node-desc">{html_module.escape(sub_definition)}</div>
  1477. ''')
  1478. if post_ids:
  1479. html_parts.append(f'''
  1480. <div class="node-posts">
  1481. <span class="posts-label">📋 帖子ID:</span>
  1482. <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
  1483. {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
  1484. </div>
  1485. ''')
  1486. html_parts.append('</li>')
  1487. html_parts.append('</ul>')
  1488. html_parts.append('</li>')
  1489. html_parts.append('</ul>')
  1490. html_parts.append('</li>')
  1491. html_parts.append('</ul>')
  1492. html_parts.append('</div>')
  1493. return ''.join(html_parts)
  1494. def generate_html(
  1495. inspirations_data: List[Dict[str, Any]],
  1496. posts_map: Dict[str, Dict[str, Any]],
  1497. persona_data: Dict[str, Any],
  1498. output_path: str,
  1499. inspiration_to_post_data: Dict[str, Any] = None,
  1500. category_index_data: Dict[str, Any] = None,
  1501. post_to_mapping_data: Dict[str, Any] = None
  1502. ) -> str:
  1503. """
  1504. 生成完整的可视化HTML
  1505. Args:
  1506. inspirations_data: 灵感点数据列表
  1507. posts_map: 帖子数据映射
  1508. persona_data: 人设数据
  1509. output_path: 输出文件路径
  1510. Returns:
  1511. 输出文件路径
  1512. """
  1513. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  1514. # 统计信息
  1515. total_count = len(inspirations_data)
  1516. # Step1 统计
  1517. step1_excellent_count = sum(1 for d in inspirations_data
  1518. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7)
  1519. step1_good_count = sum(1 for d in inspirations_data
  1520. if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7)
  1521. step1_normal_count = sum(1 for d in inspirations_data
  1522. if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5)
  1523. step1_need_opt_count = sum(1 for d in inspirations_data
  1524. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3)
  1525. # 平均分数
  1526. total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
  1527. for d in inspirations_data)
  1528. avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
  1529. # 按Step1分数排序
  1530. inspirations_data_sorted = sorted(
  1531. inspirations_data,
  1532. key=lambda x: x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
  1533. reverse=True
  1534. )
  1535. # 生成卡片HTML
  1536. cards_html = [
  1537. generate_inspiration_card_html(data, inspiration_to_post_data, category_index_data, post_to_mapping_data)
  1538. for data in inspirations_data_sorted
  1539. ]
  1540. cards_html_str = '\n'.join(cards_html)
  1541. # 生成人设结构HTML
  1542. persona_structure_html = generate_persona_structure_html(persona_data)
  1543. # 生成JavaScript
  1544. detail_modal_js = generate_detail_modal_content_js()
  1545. # 完整HTML
  1546. html_content = f'''<!DOCTYPE html>
  1547. <html lang="zh-CN">
  1548. <head>
  1549. <meta charset="UTF-8">
  1550. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1551. <title>灵感点分析可视化</title>
  1552. <style>
  1553. * {{
  1554. margin: 0;
  1555. padding: 0;
  1556. box-sizing: border-box;
  1557. }}
  1558. body {{
  1559. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  1560. background: #f5f7fa;
  1561. color: #333;
  1562. line-height: 1.6;
  1563. margin: 0;
  1564. padding: 0;
  1565. overflow: hidden;
  1566. }}
  1567. .container {{
  1568. max-width: 100%;
  1569. height: 100vh;
  1570. margin: 0;
  1571. padding: 0;
  1572. display: flex;
  1573. }}
  1574. .main-content-layout {{
  1575. display: flex;
  1576. width: 100%;
  1577. height: 100vh;
  1578. overflow: hidden;
  1579. }}
  1580. .sidebar-nav {{
  1581. width: 400px;
  1582. background: white;
  1583. padding: 30px 20px;
  1584. box-shadow: 2px 0 10px rgba(0,0,0,0.1);
  1585. overflow-y: auto;
  1586. flex-shrink: 0;
  1587. height: 100vh;
  1588. }}
  1589. .nav-title {{
  1590. font-size: 18px;
  1591. font-weight: 700;
  1592. color: #1a1a1a;
  1593. margin-bottom: 15px;
  1594. padding-bottom: 10px;
  1595. border-bottom: 2px solid #e5e7eb;
  1596. }}
  1597. .nav-list {{
  1598. display: flex;
  1599. flex-direction: column;
  1600. gap: 12px;
  1601. }}
  1602. .nav-item {{
  1603. border-radius: 10px;
  1604. cursor: pointer;
  1605. transition: all 0.3s;
  1606. border: 2px solid #e5e7eb;
  1607. overflow: hidden;
  1608. background: white;
  1609. }}
  1610. .nav-item:hover {{
  1611. border-color: #667eea;
  1612. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
  1613. }}
  1614. .nav-item.active {{
  1615. border-color: #667eea;
  1616. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);
  1617. }}
  1618. .nav-item-header {{
  1619. padding: 12px 14px;
  1620. background: #f9fafb;
  1621. display: flex;
  1622. justify-content: space-between;
  1623. align-items: center;
  1624. gap: 8px;
  1625. }}
  1626. .nav-item.active .nav-item-header {{
  1627. background: rgba(102, 126, 234, 0.1);
  1628. }}
  1629. .nav-item-name {{
  1630. flex: 1;
  1631. font-weight: 600;
  1632. font-size: 14px;
  1633. color: #1f2937;
  1634. overflow: hidden;
  1635. text-overflow: ellipsis;
  1636. white-space: nowrap;
  1637. display: flex;
  1638. align-items: center;
  1639. gap: 6px;
  1640. }}
  1641. .nav-item.active .nav-item-name {{
  1642. color: #667eea;
  1643. }}
  1644. .nav-label {{
  1645. display: inline-block;
  1646. font-size: 11px;
  1647. color: #8b5cf6;
  1648. background: #f5f3ff;
  1649. padding: 2px 6px;
  1650. border-radius: 3px;
  1651. font-weight: 500;
  1652. white-space: nowrap;
  1653. flex-shrink: 0;
  1654. }}
  1655. .nav-item-score {{
  1656. font-size: 13px;
  1657. font-weight: 700;
  1658. padding: 4px 10px;
  1659. border-radius: 6px;
  1660. background: #e5e7eb;
  1661. color: #6b7280;
  1662. }}
  1663. .nav-item.active .nav-item-score {{
  1664. background: #667eea;
  1665. color: white;
  1666. }}
  1667. .nav-item-matches {{
  1668. padding: 10px 14px;
  1669. background: white;
  1670. border-top: 1px solid #e5e7eb;
  1671. }}
  1672. .nav-match-item {{
  1673. padding: 8px 0;
  1674. font-size: 12px;
  1675. color: #6b7280;
  1676. display: flex;
  1677. align-items: flex-start;
  1678. gap: 6px;
  1679. }}
  1680. .nav-match-item:not(:last-child) {{
  1681. border-bottom: 1px dashed #e5e7eb;
  1682. }}
  1683. .nav-match-rank {{
  1684. font-weight: 700;
  1685. color: #9ca3af;
  1686. min-width: 20px;
  1687. flex-shrink: 0;
  1688. }}
  1689. .nav-match-rank.rank-1 {{
  1690. color: #f59e0b;
  1691. }}
  1692. .nav-match-rank.rank-2 {{
  1693. color: #c0c0c0;
  1694. }}
  1695. .nav-match-rank.rank-3 {{
  1696. color: #cd7f32;
  1697. }}
  1698. .nav-match-content {{
  1699. flex: 1;
  1700. line-height: 1.6;
  1701. min-width: 0;
  1702. }}
  1703. .nav-match-name {{
  1704. color: #1f2937;
  1705. font-weight: 600;
  1706. display: block;
  1707. margin-bottom: 4px;
  1708. }}
  1709. .nav-match-hierarchy {{
  1710. font-size: 11px;
  1711. color: #9ca3af;
  1712. display: flex;
  1713. align-items: center;
  1714. gap: 4px;
  1715. flex-wrap: wrap;
  1716. margin-bottom: 3px;
  1717. }}
  1718. .hierarchy-item {{
  1719. display: inline-flex;
  1720. align-items: center;
  1721. gap: 4px;
  1722. }}
  1723. .hierarchy-separator {{
  1724. color: #d1d5db;
  1725. }}
  1726. .nav-match-score {{
  1727. color: #667eea;
  1728. font-weight: 700;
  1729. font-size: 13px;
  1730. }}
  1731. .inspirations-section {{
  1732. flex: 1;
  1733. height: 100vh;
  1734. overflow-y: auto;
  1735. background: #f5f7fa;
  1736. position: relative;
  1737. }}
  1738. .breadcrumb-container {{
  1739. position: sticky;
  1740. top: 0;
  1741. left: 0;
  1742. right: 0;
  1743. background: white;
  1744. border-bottom: 1px solid #e5e7eb;
  1745. padding: 15px 30px;
  1746. z-index: 100;
  1747. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  1748. }}
  1749. .breadcrumb {{
  1750. display: flex;
  1751. align-items: center;
  1752. gap: 8px;
  1753. font-size: 14px;
  1754. color: #6b7280;
  1755. }}
  1756. .breadcrumb-item {{
  1757. display: flex;
  1758. align-items: center;
  1759. gap: 8px;
  1760. }}
  1761. .breadcrumb-separator {{
  1762. color: #d1d5db;
  1763. }}
  1764. .breadcrumb-current {{
  1765. color: #111827;
  1766. font-weight: 600;
  1767. }}
  1768. .breadcrumb-label {{
  1769. display: inline-block;
  1770. font-size: 12px;
  1771. color: #6366f1;
  1772. background: #eef2ff;
  1773. padding: 2px 8px;
  1774. border-radius: 4px;
  1775. font-weight: 500;
  1776. margin-right: 4px;
  1777. }}
  1778. .inspiration-content-wrapper {{
  1779. padding: 30px 40px;
  1780. }}
  1781. .inspirations-grid {{
  1782. display: none;
  1783. }}
  1784. .inspiration-display {{
  1785. width: 100%;
  1786. }}
  1787. .inspiration-detail {{
  1788. display: none;
  1789. }}
  1790. .inspiration-detail.active {{
  1791. display: block;
  1792. }}
  1793. .detail-header {{
  1794. background: white;
  1795. border-radius: 12px;
  1796. padding: 25px 30px;
  1797. margin-bottom: 15px;
  1798. box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  1799. border: 1px solid #e5e7eb;
  1800. }}
  1801. .detail-title-row {{
  1802. display: flex;
  1803. justify-content: space-between;
  1804. align-items: center;
  1805. margin-bottom: 15px;
  1806. }}
  1807. .detail-title-left, .detail-title-right {{
  1808. display: flex;
  1809. align-items: center;
  1810. gap: 10px;
  1811. }}
  1812. .detail-label {{
  1813. font-size: 12px;
  1814. color: #5b21b6;
  1815. background: rgba(109, 40, 217, 0.15);
  1816. padding: 3px 10px;
  1817. border-radius: 4px;
  1818. font-weight: 500;
  1819. }}
  1820. .detail-category {{
  1821. font-size: 18px;
  1822. font-weight: 600;
  1823. color: #374151;
  1824. }}
  1825. .detail-title {{
  1826. font-size: 24px;
  1827. font-weight: 700;
  1828. color: #111827;
  1829. margin-bottom: 12px;
  1830. }}
  1831. .detail-score-section {{
  1832. display: flex;
  1833. align-items: center;
  1834. gap: 12px;
  1835. }}
  1836. .detail-score-label {{
  1837. font-size: 13px;
  1838. color: #6b7280;
  1839. font-weight: 500;
  1840. }}
  1841. .detail-score-value {{
  1842. font-size: 20px;
  1843. font-weight: 700;
  1844. color: #374151;
  1845. }}
  1846. .card-header {{
  1847. display: flex;
  1848. justify-content: space-between;
  1849. align-items: flex-start;
  1850. margin-bottom: 20px;
  1851. gap: 12px;
  1852. }}
  1853. .inspiration-name {{
  1854. font-size: 28px;
  1855. font-weight: 800;
  1856. color: #1a1a1a;
  1857. line-height: 1.4;
  1858. flex: 1;
  1859. }}
  1860. .grade-badge {{
  1861. background: #10b981;
  1862. color: white;
  1863. padding: 6px 14px;
  1864. border-radius: 20px;
  1865. font-size: 12px;
  1866. font-weight: 700;
  1867. white-space: nowrap;
  1868. }}
  1869. .score-section {{
  1870. display: flex;
  1871. align-items: center;
  1872. gap: 25px;
  1873. margin-bottom: 20px;
  1874. padding: 20px;
  1875. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  1876. border-radius: 12px;
  1877. }}
  1878. .score-item {{
  1879. display: flex;
  1880. flex-direction: column;
  1881. align-items: center;
  1882. gap: 8px;
  1883. flex: 1;
  1884. }}
  1885. .main-score {{
  1886. display: flex;
  1887. flex-direction: column;
  1888. align-items: center;
  1889. gap: 8px;
  1890. }}
  1891. .score-circle {{
  1892. width: 90px;
  1893. height: 90px;
  1894. border-radius: 50%;
  1895. border: 6px solid #10b981;
  1896. display: flex;
  1897. align-items: center;
  1898. justify-content: center;
  1899. background: white;
  1900. }}
  1901. .score-value {{
  1902. font-size: 26px;
  1903. font-weight: 800;
  1904. color: #10b981;
  1905. }}
  1906. .score-label {{
  1907. font-size: 12px;
  1908. color: #6b7280;
  1909. font-weight: 600;
  1910. }}
  1911. .sub-scores {{
  1912. flex: 1;
  1913. display: flex;
  1914. flex-direction: column;
  1915. gap: 12px;
  1916. }}
  1917. .sub-score-item {{
  1918. display: flex;
  1919. justify-content: space-between;
  1920. align-items: center;
  1921. padding: 10px 15px;
  1922. background: white;
  1923. border-radius: 8px;
  1924. }}
  1925. .sub-score-label {{
  1926. font-size: 13px;
  1927. color: #6b7280;
  1928. font-weight: 600;
  1929. }}
  1930. .sub-score-value {{
  1931. font-size: 18px;
  1932. font-weight: 700;
  1933. color: #2563eb;
  1934. }}
  1935. .metrics-section {{
  1936. display: flex;
  1937. flex-direction: column;
  1938. gap: 10px;
  1939. margin-bottom: 15px;
  1940. }}
  1941. .metric-item {{
  1942. display: flex;
  1943. align-items: center;
  1944. gap: 8px;
  1945. font-size: 13px;
  1946. color: #4b5563;
  1947. }}
  1948. .metric-icon {{
  1949. font-size: 16px;
  1950. }}
  1951. .metric-label {{
  1952. font-weight: 600;
  1953. }}
  1954. .metric-value {{
  1955. color: #1f2937;
  1956. }}
  1957. .match-preview {{
  1958. background: #f9fafb;
  1959. padding: 12px;
  1960. border-radius: 8px;
  1961. margin-bottom: 10px;
  1962. border-left: 3px solid #8b5cf6;
  1963. }}
  1964. .match-preview-header {{
  1965. font-size: 12px;
  1966. font-weight: 600;
  1967. color: #6b7280;
  1968. margin-bottom: 6px;
  1969. }}
  1970. .match-preview-content {{
  1971. display: flex;
  1972. justify-content: space-between;
  1973. align-items: center;
  1974. }}
  1975. .match-preview-name {{
  1976. font-size: 13px;
  1977. color: #1f2937;
  1978. flex: 1;
  1979. }}
  1980. .match-preview-score {{
  1981. font-size: 16px;
  1982. font-weight: 700;
  1983. }}
  1984. .preview-parts {{
  1985. margin-top: 8px;
  1986. padding: 8px 10px;
  1987. border-radius: 6px;
  1988. font-size: 12px;
  1989. line-height: 1.6;
  1990. }}
  1991. .preview-parts.same {{
  1992. background: #f0fdf4;
  1993. color: #15803d;
  1994. border-left: 3px solid #10b981;
  1995. }}
  1996. .preview-parts.increment {{
  1997. background: #fff7ed;
  1998. color: #92400e;
  1999. border-left: 3px solid #f59e0b;
  2000. margin-top: 6px;
  2001. }}
  2002. .preview-parts strong {{
  2003. font-weight: 700;
  2004. margin-right: 6px;
  2005. }}
  2006. .score-divider {{
  2007. width: 1px;
  2008. height: 40px;
  2009. background: #e5e7eb;
  2010. }}
  2011. .inspiration-category-section {{
  2012. display: grid;
  2013. grid-template-columns: 1fr 1fr;
  2014. gap: 25px;
  2015. margin-bottom: 0;
  2016. }}
  2017. .info-detail-column {{
  2018. background: #fafbfc;
  2019. border-radius: 10px;
  2020. border: 1px solid #e5e7eb;
  2021. overflow: hidden;
  2022. }}
  2023. .info-header {{
  2024. padding: 15px 20px;
  2025. border-bottom: 1px solid #e5e7eb;
  2026. background: #f9fafb;
  2027. display: flex;
  2028. align-items: center;
  2029. gap: 10px;
  2030. }}
  2031. .info-label {{
  2032. font-size: 11px;
  2033. color: #8b5cf6;
  2034. background: #f5f3ff;
  2035. padding: 2px 6px;
  2036. border-radius: 3px;
  2037. font-weight: 500;
  2038. }}
  2039. .info-name {{
  2040. font-size: 16px;
  2041. font-weight: 600;
  2042. color: #111827;
  2043. }}
  2044. .info-content {{
  2045. padding: 20px;
  2046. }}
  2047. .info-field {{
  2048. margin-bottom: 12px;
  2049. line-height: 1.6;
  2050. }}
  2051. .info-field-label {{
  2052. font-weight: 600;
  2053. color: #374151;
  2054. margin-right: 6px;
  2055. }}
  2056. .info-field-value {{
  2057. color: #6b7280;
  2058. }}
  2059. .info-posts {{
  2060. margin-top: 20px;
  2061. }}
  2062. .info-posts-title {{
  2063. font-size: 14px;
  2064. font-weight: 600;
  2065. color: #374151;
  2066. margin-bottom: 12px;
  2067. }}
  2068. .info-posts-grid {{
  2069. display: flex;
  2070. gap: 15px;
  2071. overflow-x: auto;
  2072. overflow-y: hidden;
  2073. padding-bottom: 10px;
  2074. scroll-behavior: smooth;
  2075. }}
  2076. .info-posts-grid::-webkit-scrollbar {{
  2077. height: 6px;
  2078. }}
  2079. .info-posts-grid::-webkit-scrollbar-track {{
  2080. background: #f3f4f6;
  2081. border-radius: 3px;
  2082. }}
  2083. .info-posts-grid::-webkit-scrollbar-thumb {{
  2084. background: #d1d5db;
  2085. border-radius: 3px;
  2086. }}
  2087. .info-posts-grid::-webkit-scrollbar-thumb:hover {{
  2088. background: #9ca3af;
  2089. }}
  2090. .info-posts-grid .search-note-item {{
  2091. flex: 0 0 260px;
  2092. min-width: 260px;
  2093. }}
  2094. .matches-list {{
  2095. display: flex;
  2096. flex-direction: column;
  2097. gap: 15px;
  2098. }}
  2099. .match-item {{
  2100. background: white;
  2101. border-radius: 12px;
  2102. overflow: hidden;
  2103. box-shadow: 0 2px 8px rgba(0,0,0,0.06);
  2104. transition: all 0.3s;
  2105. margin-bottom: 20px;
  2106. }}
  2107. .match-item:hover {{
  2108. box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  2109. }}
  2110. .match-main-header {{
  2111. background: linear-gradient(135deg, #a5b4fc 0%, #c4b5fd 100%);
  2112. border-bottom: none;
  2113. padding: 16px 20px;
  2114. cursor: pointer;
  2115. display: flex;
  2116. justify-content: space-between;
  2117. align-items: center;
  2118. }}
  2119. .match-main-header:hover {{
  2120. background: linear-gradient(135deg, #8b9cfc 0%, #b39dfc 100%);
  2121. }}
  2122. .match-header-row {{
  2123. flex: 1;
  2124. display: flex;
  2125. justify-content: space-between;
  2126. align-items: center;
  2127. gap: 20px;
  2128. }}
  2129. .match-header-left, .match-header-right {{
  2130. display: flex;
  2131. align-items: center;
  2132. gap: 10px;
  2133. flex: 1;
  2134. }}
  2135. .match-header-center {{
  2136. display: flex;
  2137. flex-direction: column;
  2138. align-items: center;
  2139. gap: 4px;
  2140. padding: 0 20px;
  2141. }}
  2142. .match-score-label {{
  2143. font-size: 11px;
  2144. color: #4c1d95;
  2145. text-transform: uppercase;
  2146. }}
  2147. .match-score-value {{
  2148. font-size: 20px;
  2149. font-weight: 700;
  2150. color: #5b21b6;
  2151. }}
  2152. .match-title {{
  2153. color: #3730a3;
  2154. font-size: 16px;
  2155. font-weight: 700;
  2156. }}
  2157. .match-category {{
  2158. color: #3730a3;
  2159. font-size: 16px;
  2160. font-weight: 700;
  2161. }}
  2162. .match-hierarchy {{
  2163. color: #4c1d95;
  2164. font-size: 13px;
  2165. }}
  2166. .match-toggle-main {{
  2167. color: #5b21b6;
  2168. font-size: 20px;
  2169. transition: transform 0.3s;
  2170. flex-shrink: 0;
  2171. }}
  2172. .match-item.expanded .match-toggle-main {{
  2173. transform: rotate(180deg);
  2174. }}
  2175. .match-main-content {{
  2176. max-height: 0;
  2177. overflow: hidden;
  2178. transition: max-height 0.3s ease;
  2179. }}
  2180. .match-item.expanded .match-main-content {{
  2181. max-height: 10000px;
  2182. }}
  2183. .match-main-content .inspiration-category-section {{
  2184. margin: 25px;
  2185. margin-bottom: 0;
  2186. }}
  2187. .step-section-wrapper {{
  2188. margin: 20px 25px;
  2189. border: 2px solid #e5e7eb;
  2190. border-radius: 12px;
  2191. overflow: hidden;
  2192. background: white;
  2193. }}
  2194. .step-1-wrapper {{
  2195. border-color: #fbbf24;
  2196. }}
  2197. .step-2-wrapper {{
  2198. border-color: #34d399;
  2199. }}
  2200. .step-header {{
  2201. background: linear-gradient(135deg, #fbbf24 0%, #fcd34d 100%);
  2202. padding: 16px 25px;
  2203. display: flex;
  2204. justify-content: space-between;
  2205. align-items: center;
  2206. cursor: pointer;
  2207. transition: all 0.3s;
  2208. }}
  2209. .step-header:hover {{
  2210. background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
  2211. }}
  2212. .step-2-wrapper .step-header {{
  2213. background: linear-gradient(135deg, #34d399 0%, #6ee7b7 100%);
  2214. }}
  2215. .step-2-wrapper .step-header:hover {{
  2216. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  2217. }}
  2218. .step-header-content {{
  2219. display: flex;
  2220. align-items: center;
  2221. gap: 12px;
  2222. }}
  2223. .step-header .step-toggle {{
  2224. font-size: 20px;
  2225. color: #78350f;
  2226. transition: transform 0.3s;
  2227. }}
  2228. .step-2-wrapper .step-header .step-toggle {{
  2229. color: #065f46;
  2230. }}
  2231. .step-section-wrapper.expanded > .step-header .step-toggle {{
  2232. transform: rotate(180deg);
  2233. }}
  2234. .step-wrapper-content {{
  2235. max-height: 0;
  2236. overflow: hidden;
  2237. transition: max-height 0.3s ease;
  2238. }}
  2239. .step-section-wrapper.expanded .step-wrapper-content {{
  2240. max-height: 30000px;
  2241. }}
  2242. .step-number-badge {{
  2243. background: rgba(255, 255, 255, 0.5);
  2244. color: #78350f;
  2245. padding: 6px 14px;
  2246. border-radius: 20px;
  2247. font-size: 13px;
  2248. font-weight: 700;
  2249. letter-spacing: 0.5px;
  2250. }}
  2251. .step-2-wrapper .step-number-badge {{
  2252. color: #065f46;
  2253. }}
  2254. .step-title {{
  2255. color: #78350f;
  2256. font-size: 16px;
  2257. font-weight: 700;
  2258. }}
  2259. .step-2-wrapper .step-title {{
  2260. color: #065f46;
  2261. }}
  2262. .match-analysis-section {{
  2263. padding: 25px;
  2264. }}
  2265. .match-header {{
  2266. padding: 20px;
  2267. cursor: pointer;
  2268. display: flex;
  2269. justify-content: space-between;
  2270. align-items: center;
  2271. gap: 15px;
  2272. background: #fafafa;
  2273. transition: background 0.3s;
  2274. border-bottom: 1px solid #e5e7eb;
  2275. }}
  2276. .match-header:hover {{
  2277. background: #f5f5f5;
  2278. }}
  2279. .match-section.expanded .match-header {{
  2280. background: rgba(102, 126, 234, 0.05);
  2281. }}
  2282. .match-header-left {{
  2283. flex: 1;
  2284. min-width: 0;
  2285. }}
  2286. .match-rank {{
  2287. display: inline-block;
  2288. padding: 3px 10px;
  2289. border-radius: 4px;
  2290. font-size: 11px;
  2291. font-weight: 600;
  2292. background: rgba(91, 33, 182, 0.15);
  2293. color: #5b21b6;
  2294. }}
  2295. .match-element-name {{
  2296. font-size: 18px;
  2297. font-weight: 700;
  2298. color: #1f2937;
  2299. display: block;
  2300. margin-bottom: 6px;
  2301. }}
  2302. .match-context {{
  2303. font-size: 13px;
  2304. color: #6b7280;
  2305. display: flex;
  2306. align-items: center;
  2307. gap: 6px;
  2308. }}
  2309. .match-context-separator {{
  2310. color: #d1d5db;
  2311. }}
  2312. .match-header-right {{
  2313. display: flex;
  2314. align-items: center;
  2315. gap: 15px;
  2316. flex-shrink: 0;
  2317. }}
  2318. .match-score {{
  2319. font-size: 24px;
  2320. font-weight: 800;
  2321. color: #667eea;
  2322. }}
  2323. .match-toggle {{
  2324. width: 32px;
  2325. height: 32px;
  2326. border-radius: 8px;
  2327. background: #e5e7eb;
  2328. display: flex;
  2329. align-items: center;
  2330. justify-content: center;
  2331. transition: all 0.3s;
  2332. font-size: 18px;
  2333. color: #6b7280;
  2334. }}
  2335. .match-item.expanded .match-toggle {{
  2336. background: #667eea;
  2337. color: white;
  2338. transform: rotate(180deg);
  2339. }}
  2340. .match-section {{
  2341. border-bottom: 1px solid #e5e7eb;
  2342. }}
  2343. .match-section:last-child {{
  2344. border-bottom: none;
  2345. }}
  2346. .step-section-header {{
  2347. padding: 18px 25px;
  2348. background: #f9fafb;
  2349. cursor: pointer;
  2350. display: flex;
  2351. justify-content: space-between;
  2352. align-items: center;
  2353. border-bottom: 1px solid #e5e7eb;
  2354. }}
  2355. .step-section-header:hover {{
  2356. background: #f3f4f6;
  2357. }}
  2358. .step-section-title {{
  2359. font-size: 15px;
  2360. font-weight: 600;
  2361. color: #374151;
  2362. display: flex;
  2363. align-items: center;
  2364. gap: 10px;
  2365. }}
  2366. .step-sub-number {{
  2367. display: inline-flex;
  2368. align-items: center;
  2369. justify-content: center;
  2370. min-width: 38px;
  2371. padding: 4px 10px;
  2372. background: #10b981;
  2373. color: white;
  2374. border-radius: 6px;
  2375. font-size: 12px;
  2376. font-weight: 700;
  2377. }}
  2378. .step-number {{
  2379. display: inline-flex;
  2380. align-items: center;
  2381. justify-content: center;
  2382. width: 20px;
  2383. height: 20px;
  2384. background: #e5e7eb;
  2385. color: #6b7280;
  2386. border-radius: 50%;
  2387. font-size: 11px;
  2388. font-weight: 600;
  2389. }}
  2390. .step-toggle {{
  2391. font-size: 18px;
  2392. color: #6b7280;
  2393. transition: transform 0.3s;
  2394. }}
  2395. .step-section.expanded .step-toggle {{
  2396. transform: rotate(180deg);
  2397. }}
  2398. .step-section-content {{
  2399. max-height: 0;
  2400. overflow: hidden;
  2401. transition: max-height 0.3s ease;
  2402. }}
  2403. .step-section.expanded .step-section-content {{
  2404. max-height: 20000px;
  2405. }}
  2406. .match-content {{
  2407. max-height: 0;
  2408. overflow: hidden;
  2409. transition: max-height 0.3s ease;
  2410. }}
  2411. .match-section.expanded .match-content {{
  2412. max-height: 2000px;
  2413. }}
  2414. .match-content-inner {{
  2415. padding: 20px;
  2416. }}
  2417. .match-explain {{
  2418. background: #fffbeb;
  2419. padding: 16px 20px;
  2420. border-radius: 8px;
  2421. margin-top: 20px;
  2422. border-left: 3px solid #f59e0b;
  2423. }}
  2424. .match-explain-title {{
  2425. font-weight: 600;
  2426. color: #374151;
  2427. margin-bottom: 6px;
  2428. font-size: 13px;
  2429. }}
  2430. .match-explain-text {{
  2431. color: #6b7280;
  2432. font-size: 14px;
  2433. line-height: 1.7;
  2434. }}
  2435. .match-parts-container {{
  2436. display: grid;
  2437. grid-template-columns: 1fr 1fr;
  2438. gap: 25px;
  2439. margin-bottom: 20px;
  2440. }}
  2441. .match-parts-column {{
  2442. min-width: 0;
  2443. }}
  2444. .match-parts {{
  2445. margin-bottom: 0;
  2446. height: 100%;
  2447. background: #f9fafb;
  2448. padding: 16px;
  2449. border-radius: 8px;
  2450. border: 1px solid #e5e7eb;
  2451. }}
  2452. .match-parts-title {{
  2453. font-weight: 700;
  2454. font-size: 14px;
  2455. margin-bottom: 16px;
  2456. color: #374151;
  2457. text-transform: uppercase;
  2458. letter-spacing: 0.5px;
  2459. }}
  2460. .part-item {{
  2461. padding: 10px 0;
  2462. margin-bottom: 8px;
  2463. font-size: 14px;
  2464. border-bottom: 1px solid #e5e7eb;
  2465. }}
  2466. .part-item:last-child {{
  2467. border-bottom: none;
  2468. }}
  2469. .part-key {{
  2470. font-weight: 500;
  2471. color: #374151;
  2472. margin-right: 8px;
  2473. }}
  2474. .part-value {{
  2475. color: #6b7280;
  2476. }}
  2477. .same-parts {{
  2478. background: #ecfdf5;
  2479. border-color: #10b981;
  2480. }}
  2481. .increment-parts {{
  2482. background: #fef3c7;
  2483. border-color: #f59e0b;
  2484. }}
  2485. .search-results-section {{
  2486. padding: 25px;
  2487. }}
  2488. .search-params-section {{
  2489. background: #f0f9ff;
  2490. border: 1px solid #bae6fd;
  2491. border-radius: 8px;
  2492. padding: 20px;
  2493. margin-bottom: 20px;
  2494. }}
  2495. .search-params-title {{
  2496. font-size: 15px;
  2497. font-weight: 700;
  2498. color: #0c4a6e;
  2499. margin-bottom: 16px;
  2500. }}
  2501. .search-params-grid {{
  2502. display: grid;
  2503. grid-template-columns: repeat(2, 1fr);
  2504. gap: 12px;
  2505. }}
  2506. .search-param-item {{
  2507. display: flex;
  2508. align-items: center;
  2509. gap: 8px;
  2510. }}
  2511. .search-param-label {{
  2512. font-size: 13px;
  2513. color: #0369a1;
  2514. font-weight: 600;
  2515. min-width: 80px;
  2516. }}
  2517. .search-param-value {{
  2518. font-size: 13px;
  2519. color: #0c4a6e;
  2520. background: #e0f2fe;
  2521. padding: 4px 12px;
  2522. border-radius: 4px;
  2523. }}
  2524. .search-summary-section {{
  2525. background: #ecfdf5;
  2526. border: 1px solid #a7f3d0;
  2527. border-radius: 8px;
  2528. padding: 16px 20px;
  2529. margin-bottom: 20px;
  2530. }}
  2531. .search-summary-header {{
  2532. display: flex;
  2533. justify-content: space-between;
  2534. align-items: center;
  2535. gap: 20px;
  2536. }}
  2537. .search-summary-left {{
  2538. flex: 1;
  2539. }}
  2540. .search-summary-title {{
  2541. font-size: 15px;
  2542. font-weight: 700;
  2543. color: #065f46;
  2544. margin-bottom: 8px;
  2545. }}
  2546. .search-summary-content {{
  2547. font-size: 14px;
  2548. color: #047857;
  2549. }}
  2550. .search-result-count {{
  2551. font-size: 18px;
  2552. font-weight: 700;
  2553. color: #059669;
  2554. margin: 0 4px;
  2555. }}
  2556. .search-sort-buttons {{
  2557. display: flex;
  2558. align-items: center;
  2559. gap: 8px;
  2560. }}
  2561. .search-sort-label {{
  2562. font-size: 13px;
  2563. color: #047857;
  2564. font-weight: 600;
  2565. }}
  2566. .search-sort-btn {{
  2567. background: white;
  2568. border: 1px solid #a7f3d0;
  2569. color: #065f46;
  2570. padding: 6px 12px;
  2571. border-radius: 6px;
  2572. font-size: 12px;
  2573. font-weight: 600;
  2574. cursor: pointer;
  2575. transition: all 0.2s;
  2576. }}
  2577. .search-sort-btn:hover {{
  2578. background: #d1fae5;
  2579. border-color: #6ee7b7;
  2580. }}
  2581. .search-sort-btn.active {{
  2582. background: #059669;
  2583. color: white;
  2584. border-color: #059669;
  2585. }}
  2586. .note-position {{
  2587. font-size: 11px;
  2588. color: #9ca3af;
  2589. font-weight: 600;
  2590. }}
  2591. .search-notes-list {{
  2592. display: grid;
  2593. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  2594. gap: 16px;
  2595. }}
  2596. .search-note-item {{
  2597. background: white;
  2598. border-radius: 12px;
  2599. overflow: hidden;
  2600. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  2601. border: 1px solid #e5e7eb;
  2602. transition: all 0.3s;
  2603. position: relative;
  2604. }}
  2605. .search-note-item:hover {{
  2606. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
  2607. border-color: #667eea;
  2608. }}
  2609. .note-score-badge {{
  2610. position: absolute;
  2611. top: 12px;
  2612. right: 12px;
  2613. background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  2614. padding: 8px 14px;
  2615. border-radius: 8px;
  2616. display: flex;
  2617. flex-direction: column;
  2618. align-items: center;
  2619. gap: 2px;
  2620. cursor: pointer;
  2621. z-index: 5;
  2622. box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
  2623. transition: all 0.3s;
  2624. }}
  2625. .note-score-badge:hover {{
  2626. transform: scale(1.05);
  2627. box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
  2628. }}
  2629. .note-score-badge .score-label {{
  2630. font-size: 10px;
  2631. color: rgba(255, 255, 255, 0.9);
  2632. text-transform: uppercase;
  2633. letter-spacing: 0.5px;
  2634. }}
  2635. .note-score-badge .score-value {{
  2636. font-size: 18px;
  2637. font-weight: 700;
  2638. color: white;
  2639. }}
  2640. .note-image-carousel {{
  2641. position: relative;
  2642. width: 100%;
  2643. min-height: 200px;
  2644. max-height: 400px;
  2645. background: #f3f4f6;
  2646. overflow: hidden;
  2647. display: flex;
  2648. align-items: center;
  2649. justify-content: center;
  2650. }}
  2651. .note-images-track {{
  2652. display: flex;
  2653. width: 100%;
  2654. height: 100%;
  2655. transition: transform 0.4s ease;
  2656. }}
  2657. .note-image {{
  2658. flex: 0 0 100%;
  2659. width: 100%;
  2660. height: auto;
  2661. max-height: 400px;
  2662. object-fit: contain;
  2663. }}
  2664. .note-carousel-button {{
  2665. position: absolute;
  2666. top: 50%;
  2667. transform: translateY(-50%);
  2668. width: 32px;
  2669. height: 32px;
  2670. background: rgba(255, 255, 255, 0.9);
  2671. border: none;
  2672. border-radius: 50%;
  2673. display: flex;
  2674. align-items: center;
  2675. justify-content: center;
  2676. cursor: pointer;
  2677. transition: all 0.3s;
  2678. z-index: 10;
  2679. font-size: 16px;
  2680. color: #6b7280;
  2681. opacity: 0;
  2682. }}
  2683. .search-note-item:hover .note-carousel-button {{
  2684. opacity: 1;
  2685. }}
  2686. .note-carousel-button:hover {{
  2687. background: #667eea;
  2688. color: white;
  2689. }}
  2690. .note-carousel-button.disabled {{
  2691. opacity: 0 !important;
  2692. }}
  2693. .note-carousel-button.prev {{
  2694. left: 10px;
  2695. }}
  2696. .note-carousel-button.next {{
  2697. right: 10px;
  2698. }}
  2699. .note-image-dots {{
  2700. position: absolute;
  2701. bottom: 10px;
  2702. left: 50%;
  2703. transform: translateX(-50%);
  2704. display: flex;
  2705. gap: 6px;
  2706. z-index: 10;
  2707. }}
  2708. .note-image-dot {{
  2709. width: 6px;
  2710. height: 6px;
  2711. border-radius: 50%;
  2712. background: rgba(255, 255, 255, 0.6);
  2713. transition: all 0.3s;
  2714. }}
  2715. .note-image-dot.active {{
  2716. background: white;
  2717. width: 20px;
  2718. border-radius: 3px;
  2719. }}
  2720. .note-content {{
  2721. padding: 15px;
  2722. }}
  2723. .note-title {{
  2724. font-size: 15px;
  2725. font-weight: 600;
  2726. color: #1f2937;
  2727. margin-bottom: 8px;
  2728. overflow: hidden;
  2729. text-overflow: ellipsis;
  2730. display: -webkit-box;
  2731. -webkit-line-clamp: 2;
  2732. -webkit-box-orient: vertical;
  2733. line-height: 1.4;
  2734. }}
  2735. .note-desc {{
  2736. font-size: 13px;
  2737. color: #6b7280;
  2738. line-height: 1.6;
  2739. margin-bottom: 12px;
  2740. overflow: hidden;
  2741. text-overflow: ellipsis;
  2742. display: -webkit-box;
  2743. -webkit-line-clamp: 2;
  2744. -webkit-box-orient: vertical;
  2745. }}
  2746. .note-publish-date {{
  2747. font-size: 12px;
  2748. color: #9ca3af;
  2749. margin-bottom: 12px;
  2750. display: flex;
  2751. align-items: center;
  2752. gap: 4px;
  2753. }}
  2754. .note-footer {{
  2755. display: flex;
  2756. justify-content: space-between;
  2757. align-items: center;
  2758. padding-top: 12px;
  2759. border-top: 1px solid #f3f4f6;
  2760. }}
  2761. .note-author {{
  2762. font-size: 12px;
  2763. font-weight: 600;
  2764. color: #6b7280;
  2765. overflow: hidden;
  2766. text-overflow: ellipsis;
  2767. white-space: nowrap;
  2768. max-width: 150px;
  2769. }}
  2770. .note-points-hover {{
  2771. position: absolute;
  2772. bottom: 100%;
  2773. left: 0;
  2774. right: 0;
  2775. background: white;
  2776. border: 1px solid #e5e7eb;
  2777. border-radius: 8px;
  2778. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2779. padding: 12px;
  2780. margin-bottom: 8px;
  2781. opacity: 0;
  2782. visibility: hidden;
  2783. transition: opacity 0.2s, visibility 0.2s;
  2784. z-index: 10;
  2785. max-height: 300px;
  2786. overflow-y: auto;
  2787. }}
  2788. .search-note-item {{
  2789. position: relative;
  2790. }}
  2791. .search-note-item:hover .note-points-hover {{
  2792. opacity: 1;
  2793. visibility: visible;
  2794. }}
  2795. .note-points {{
  2796. /* 用于详情弹窗中的样式 */
  2797. }}
  2798. .points-section {{
  2799. margin-bottom: 8px;
  2800. }}
  2801. .points-section:last-child {{
  2802. margin-bottom: 0;
  2803. }}
  2804. .points-label {{
  2805. font-size: 11px;
  2806. font-weight: 600;
  2807. color: #6b7280;
  2808. margin-bottom: 4px;
  2809. }}
  2810. .points-list {{
  2811. display: flex;
  2812. flex-wrap: wrap;
  2813. gap: 4px;
  2814. }}
  2815. .point-item {{
  2816. display: inline-block;
  2817. }}
  2818. .point-name {{
  2819. display: inline-block;
  2820. font-size: 11px;
  2821. color: #4b5563;
  2822. background: #f3f4f6;
  2823. padding: 2px 6px;
  2824. border-radius: 3px;
  2825. }}
  2826. .note-stats {{
  2827. display: flex;
  2828. gap: 10px;
  2829. font-size: 12px;
  2830. color: #9ca3af;
  2831. }}
  2832. .note-link {{
  2833. display: block;
  2834. text-align: center;
  2835. padding: 8px;
  2836. margin-top: 10px;
  2837. background: rgba(102, 126, 234, 0.1);
  2838. color: #667eea;
  2839. text-decoration: none;
  2840. border-radius: 6px;
  2841. font-size: 13px;
  2842. font-weight: 600;
  2843. transition: all 0.3s;
  2844. }}
  2845. .note-link:hover {{
  2846. background: #667eea;
  2847. color: white;
  2848. }}
  2849. /* 笔记详情Modal样式 */
  2850. .note-detail-modal {{
  2851. display: none;
  2852. position: fixed;
  2853. top: 0;
  2854. left: 0;
  2855. width: 100%;
  2856. height: 100%;
  2857. background: rgba(0, 0, 0, 0.8);
  2858. z-index: 9999;
  2859. overflow-y: auto;
  2860. padding: 40px 20px;
  2861. }}
  2862. .note-detail-modal.active {{
  2863. display: flex;
  2864. align-items: flex-start;
  2865. justify-content: center;
  2866. }}
  2867. .note-detail-content {{
  2868. background: white;
  2869. border-radius: 16px;
  2870. max-width: 1200px;
  2871. width: 100%;
  2872. position: relative;
  2873. margin: auto;
  2874. }}
  2875. .note-detail-close {{
  2876. position: absolute;
  2877. top: 20px;
  2878. right: 20px;
  2879. width: 40px;
  2880. height: 40px;
  2881. background: white;
  2882. border: none;
  2883. border-radius: 50%;
  2884. font-size: 24px;
  2885. cursor: pointer;
  2886. display: flex;
  2887. align-items: center;
  2888. justify-content: center;
  2889. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  2890. transition: all 0.3s;
  2891. z-index: 10;
  2892. }}
  2893. .note-detail-close:hover {{
  2894. background: #ef4444;
  2895. color: white;
  2896. transform: rotate(90deg);
  2897. }}
  2898. .note-detail-header {{
  2899. padding: 30px;
  2900. border-bottom: 1px solid #e5e7eb;
  2901. }}
  2902. .note-detail-title {{
  2903. font-size: 24px;
  2904. font-weight: 800;
  2905. color: #1f2937;
  2906. margin-bottom: 15px;
  2907. }}
  2908. .note-detail-meta {{
  2909. display: flex;
  2910. align-items: center;
  2911. gap: 20px;
  2912. font-size: 14px;
  2913. color: #6b7280;
  2914. }}
  2915. .note-detail-author {{
  2916. font-weight: 600;
  2917. color: #1f2937;
  2918. }}
  2919. .note-detail-stats {{
  2920. display: flex;
  2921. gap: 15px;
  2922. }}
  2923. .note-detail-body {{
  2924. padding: 30px;
  2925. }}
  2926. .note-detail-desc {{
  2927. font-size: 15px;
  2928. line-height: 1.8;
  2929. color: #4b5563;
  2930. margin-bottom: 30px;
  2931. }}
  2932. .note-detail-points {{
  2933. margin-bottom: 30px;
  2934. background: #f9fafb;
  2935. border-radius: 8px;
  2936. padding: 20px;
  2937. }}
  2938. .detail-points-section {{
  2939. margin-bottom: 20px;
  2940. }}
  2941. .detail-points-section:last-child {{
  2942. margin-bottom: 0;
  2943. }}
  2944. .detail-points-title {{
  2945. font-size: 16px;
  2946. font-weight: 600;
  2947. color: #111827;
  2948. margin-bottom: 12px;
  2949. }}
  2950. .detail-point-item {{
  2951. background: white;
  2952. padding: 12px 15px;
  2953. border-radius: 6px;
  2954. margin-bottom: 10px;
  2955. border-left: 3px solid #e5e7eb;
  2956. }}
  2957. .detail-point-item:last-child {{
  2958. margin-bottom: 0;
  2959. }}
  2960. .detail-point-name {{
  2961. font-size: 14px;
  2962. font-weight: 600;
  2963. color: #374151;
  2964. margin-bottom: 4px;
  2965. }}
  2966. .detail-point-desc {{
  2967. font-size: 13px;
  2968. line-height: 1.6;
  2969. color: #6b7280;
  2970. }}
  2971. .note-detail-images {{
  2972. display: grid;
  2973. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  2974. gap: 15px;
  2975. }}
  2976. .note-detail-image {{
  2977. width: 100%;
  2978. border-radius: 8px;
  2979. cursor: pointer;
  2980. transition: all 0.3s;
  2981. }}
  2982. .note-detail-image:hover {{
  2983. transform: scale(1.05);
  2984. box-shadow: 0 8px 24px rgba(0,0,0,0.2);
  2985. }}
  2986. .note-detail-footer {{
  2987. padding: 20px 30px;
  2988. border-top: 1px solid #e5e7eb;
  2989. display: flex;
  2990. justify-content: center;
  2991. }}
  2992. .note-detail-link {{
  2993. display: inline-block;
  2994. padding: 12px 32px;
  2995. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2996. color: white;
  2997. text-decoration: none;
  2998. border-radius: 10px;
  2999. font-weight: 600;
  3000. transition: all 0.3s;
  3001. }}
  3002. .note-detail-link:hover {{
  3003. transform: translateY(-2px);
  3004. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  3005. }}
  3006. /* Modal样式 */
  3007. .modal-overlay {{
  3008. display: none;
  3009. position: fixed;
  3010. top: 0;
  3011. left: 0;
  3012. right: 0;
  3013. bottom: 0;
  3014. background: rgba(0, 0, 0, 0.8);
  3015. z-index: 1000;
  3016. align-items: center;
  3017. justify-content: center;
  3018. padding: 20px;
  3019. overflow-y: auto;
  3020. }}
  3021. .modal-overlay.active {{
  3022. display: flex;
  3023. }}
  3024. .modal-content {{
  3025. background: white;
  3026. border-radius: 16px;
  3027. max-width: 1200px;
  3028. width: 100%;
  3029. max-height: 90vh;
  3030. overflow-y: auto;
  3031. position: relative;
  3032. }}
  3033. .modal-close {{
  3034. position: sticky;
  3035. top: 0;
  3036. right: 0;
  3037. background: white;
  3038. border: none;
  3039. font-size: 32px;
  3040. color: #6b7280;
  3041. cursor: pointer;
  3042. padding: 15px 20px;
  3043. z-index: 10;
  3044. text-align: right;
  3045. border-bottom: 1px solid #e5e7eb;
  3046. }}
  3047. .modal-close:hover {{
  3048. color: #1f2937;
  3049. }}
  3050. .modal-body {{
  3051. padding: 30px;
  3052. }}
  3053. /* 分数详情Modal样式 */
  3054. .score-detail-content {{
  3055. max-width: 800px;
  3056. }}
  3057. .score-detail-container {{
  3058. padding: 10px;
  3059. }}
  3060. .score-detail-title {{
  3061. font-size: 24px;
  3062. font-weight: 700;
  3063. color: #1f2937;
  3064. margin-bottom: 20px;
  3065. padding-bottom: 15px;
  3066. border-bottom: 2px solid #e5e7eb;
  3067. }}
  3068. .score-detail-score {{
  3069. background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  3070. padding: 20px;
  3071. border-radius: 12px;
  3072. margin-bottom: 20px;
  3073. display: flex;
  3074. align-items: center;
  3075. justify-content: center;
  3076. gap: 10px;
  3077. }}
  3078. .score-detail-label {{
  3079. font-size: 16px;
  3080. color: rgba(255, 255, 255, 0.9);
  3081. font-weight: 600;
  3082. }}
  3083. .score-detail-value {{
  3084. font-size: 32px;
  3085. font-weight: 700;
  3086. color: white;
  3087. }}
  3088. .score-detail-explain {{
  3089. background: #fffbeb;
  3090. padding: 16px 20px;
  3091. border-radius: 8px;
  3092. margin-bottom: 20px;
  3093. border-left: 3px solid #f59e0b;
  3094. }}
  3095. .score-input-info {{
  3096. background: #f0f9ff;
  3097. padding: 16px 20px;
  3098. border-radius: 8px;
  3099. margin-bottom: 20px;
  3100. border-left: 3px solid #3b82f6;
  3101. }}
  3102. .score-input-title {{
  3103. font-weight: 700;
  3104. font-size: 14px;
  3105. margin-bottom: 12px;
  3106. color: #1e40af;
  3107. text-transform: uppercase;
  3108. letter-spacing: 0.5px;
  3109. }}
  3110. .score-input-item {{
  3111. margin-bottom: 12px;
  3112. padding-bottom: 12px;
  3113. border-bottom: 1px solid #bfdbfe;
  3114. }}
  3115. .score-input-item:last-child {{
  3116. margin-bottom: 0;
  3117. padding-bottom: 0;
  3118. border-bottom: none;
  3119. }}
  3120. .score-input-label {{
  3121. font-weight: 600;
  3122. font-size: 13px;
  3123. color: #1e40af;
  3124. margin-bottom: 4px;
  3125. }}
  3126. .score-input-value {{
  3127. font-size: 14px;
  3128. color: #374151;
  3129. line-height: 1.6;
  3130. white-space: pre-wrap;
  3131. word-break: break-word;
  3132. }}
  3133. .score-parts-container {{
  3134. display: grid;
  3135. grid-template-columns: 1fr 1fr;
  3136. gap: 20px;
  3137. }}
  3138. .score-parts {{
  3139. background: #f9fafb;
  3140. padding: 16px;
  3141. border-radius: 8px;
  3142. border: 1px solid #e5e7eb;
  3143. }}
  3144. .score-parts.same-parts {{
  3145. background: #ecfdf5;
  3146. border-color: #10b981;
  3147. }}
  3148. .score-parts.increment-parts {{
  3149. background: #fef3c7;
  3150. border-color: #f59e0b;
  3151. }}
  3152. .score-parts-title {{
  3153. font-weight: 700;
  3154. font-size: 14px;
  3155. margin-bottom: 16px;
  3156. color: #374151;
  3157. text-transform: uppercase;
  3158. letter-spacing: 0.5px;
  3159. }}
  3160. .score-part-item {{
  3161. padding: 10px 0;
  3162. margin-bottom: 8px;
  3163. font-size: 14px;
  3164. border-bottom: 1px solid #e5e7eb;
  3165. }}
  3166. .score-part-item:last-child {{
  3167. border-bottom: none;
  3168. }}
  3169. .score-part-key {{
  3170. font-weight: 500;
  3171. color: #374151;
  3172. margin-right: 8px;
  3173. }}
  3174. .score-part-value {{
  3175. color: #6b7280;
  3176. }}
  3177. .modal-header {{
  3178. margin-bottom: 25px;
  3179. padding-bottom: 20px;
  3180. border-bottom: 2px solid #e5e7eb;
  3181. }}
  3182. .modal-title {{
  3183. font-size: 28px;
  3184. font-weight: 800;
  3185. color: #1a1a1a;
  3186. }}
  3187. .modal-section {{
  3188. margin-bottom: 30px;
  3189. }}
  3190. .modal-section h3 {{
  3191. font-size: 20px;
  3192. font-weight: 700;
  3193. color: #374151;
  3194. margin-bottom: 15px;
  3195. padding-bottom: 10px;
  3196. border-bottom: 2px solid #f3f4f6;
  3197. }}
  3198. .info-grid {{
  3199. display: grid;
  3200. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  3201. gap: 15px;
  3202. }}
  3203. .info-item {{
  3204. background: #f9fafb;
  3205. padding: 12px 16px;
  3206. border-radius: 8px;
  3207. border-left: 3px solid #8b5cf6;
  3208. }}
  3209. .info-label {{
  3210. font-weight: 600;
  3211. color: #6b7280;
  3212. font-size: 13px;
  3213. margin-right: 8px;
  3214. }}
  3215. .info-value {{
  3216. color: #1f2937;
  3217. font-size: 14px;
  3218. }}
  3219. .metrics-grid {{
  3220. display: grid;
  3221. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  3222. gap: 15px;
  3223. }}
  3224. .metric-box {{
  3225. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3226. padding: 20px;
  3227. border-radius: 12px;
  3228. text-align: center;
  3229. color: white;
  3230. }}
  3231. .metric-box.wide {{
  3232. grid-column: span 2;
  3233. }}
  3234. .metric-box-label {{
  3235. font-size: 13px;
  3236. opacity: 0.9;
  3237. margin-bottom: 8px;
  3238. font-weight: 600;
  3239. }}
  3240. .metric-box-value {{
  3241. font-size: 28px;
  3242. font-weight: 700;
  3243. }}
  3244. .metric-box-value.small {{
  3245. font-size: 16px;
  3246. }}
  3247. .step-content {{
  3248. background: #f9fafb;
  3249. padding: 20px;
  3250. border-radius: 12px;
  3251. }}
  3252. .step-field {{
  3253. margin-bottom: 20px;
  3254. }}
  3255. .step-field-label {{
  3256. font-weight: 700;
  3257. color: #374151;
  3258. font-size: 14px;
  3259. margin-bottom: 8px;
  3260. display: block;
  3261. }}
  3262. .step-field-value {{
  3263. color: #1f2937;
  3264. font-size: 15px;
  3265. line-height: 1.7;
  3266. }}
  3267. .matches-list {{
  3268. display: flex;
  3269. flex-direction: column;
  3270. gap: 15px;
  3271. margin-top: 10px;
  3272. }}
  3273. .match-item {{
  3274. background: white;
  3275. padding: 18px;
  3276. border-radius: 10px;
  3277. border-left: 5px solid #3b82f6;
  3278. }}
  3279. .match-item.top1 {{
  3280. border-left-color: #fbbf24;
  3281. background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
  3282. }}
  3283. .match-item.top2 {{
  3284. border-left-color: #c0c0c0;
  3285. background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
  3286. }}
  3287. .match-item.top3 {{
  3288. border-left-color: #cd7f32;
  3289. background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
  3290. }}
  3291. .match-header {{
  3292. display: flex;
  3293. justify-content: space-between;
  3294. align-items: center;
  3295. margin-bottom: 12px;
  3296. gap: 10px;
  3297. }}
  3298. .match-rank {{
  3299. font-size: 18px;
  3300. font-weight: 800;
  3301. color: #6b7280;
  3302. }}
  3303. .match-element-name {{
  3304. flex: 1;
  3305. font-size: 16px;
  3306. font-weight: 700;
  3307. color: #1f2937;
  3308. }}
  3309. .match-score {{
  3310. font-size: 22px;
  3311. font-weight: 800;
  3312. color: #2563eb;
  3313. background: white;
  3314. padding: 6px 14px;
  3315. border-radius: 8px;
  3316. }}
  3317. .match-detail {{
  3318. background: rgba(255, 255, 255, 0.7);
  3319. padding: 10px;
  3320. border-radius: 6px;
  3321. margin-bottom: 10px;
  3322. font-size: 13px;
  3323. color: #4b5563;
  3324. }}
  3325. .match-reason {{
  3326. color: #1f2937;
  3327. font-size: 14px;
  3328. line-height: 1.7;
  3329. }}
  3330. .increment-matches {{
  3331. display: flex;
  3332. flex-direction: column;
  3333. gap: 12px;
  3334. margin-top: 10px;
  3335. }}
  3336. .increment-item {{
  3337. background: white;
  3338. padding: 15px;
  3339. border-radius: 8px;
  3340. border-left: 4px solid #10b981;
  3341. }}
  3342. .increment-header {{
  3343. display: flex;
  3344. justify-content: space-between;
  3345. align-items: center;
  3346. margin-bottom: 10px;
  3347. }}
  3348. .increment-words {{
  3349. font-weight: 700;
  3350. color: #1f2937;
  3351. font-size: 15px;
  3352. }}
  3353. .increment-score {{
  3354. font-size: 20px;
  3355. font-weight: 800;
  3356. color: #10b981;
  3357. }}
  3358. .increment-reason {{
  3359. color: #4b5563;
  3360. font-size: 13px;
  3361. line-height: 1.6;
  3362. }}
  3363. .empty-state {{
  3364. text-align: center;
  3365. padding: 40px;
  3366. color: #9ca3af;
  3367. font-size: 14px;
  3368. }}
  3369. .modal-link {{
  3370. margin-top: 25px;
  3371. padding-top: 20px;
  3372. border-top: 2px solid #e5e7eb;
  3373. text-align: center;
  3374. }}
  3375. .modal-link-btn {{
  3376. display: inline-flex;
  3377. align-items: center;
  3378. gap: 10px;
  3379. padding: 12px 24px;
  3380. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3381. color: white;
  3382. text-decoration: none;
  3383. border-radius: 10px;
  3384. font-size: 15px;
  3385. font-weight: 600;
  3386. transition: all 0.3s;
  3387. }}
  3388. .modal-link-btn:hover {{
  3389. transform: translateY(-2px);
  3390. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  3391. }}
  3392. .timestamp {{
  3393. text-align: center;
  3394. color: white;
  3395. font-size: 13px;
  3396. margin-top: 30px;
  3397. opacity: 0.8;
  3398. }}
  3399. .match-context {{
  3400. background: #f3f4f6;
  3401. padding: 8px 12px;
  3402. border-radius: 6px;
  3403. margin: 8px 0;
  3404. font-size: 12px;
  3405. color: #6b7280;
  3406. line-height: 1.6;
  3407. }}
  3408. .match-explain {{
  3409. background: #fef3c7;
  3410. padding: 10px 12px;
  3411. border-radius: 6px;
  3412. margin: 10px 0;
  3413. font-size: 13px;
  3414. color: #92400e;
  3415. line-height: 1.7;
  3416. border-left: 3px solid #f59e0b;
  3417. }}
  3418. .match-parts {{
  3419. margin: 12px 0;
  3420. border-radius: 8px;
  3421. overflow: hidden;
  3422. }}
  3423. .match-parts.same-parts {{
  3424. background: #f0fdf4;
  3425. border: 2px solid #10b981;
  3426. }}
  3427. .match-parts.increment-parts {{
  3428. background: #fff7ed;
  3429. border: 2px solid #f59e0b;
  3430. }}
  3431. .parts-header {{
  3432. font-weight: 700;
  3433. padding: 10px 12px;
  3434. font-size: 13px;
  3435. }}
  3436. .same-parts .parts-header {{
  3437. background: #dcfce7;
  3438. color: #15803d;
  3439. }}
  3440. .increment-parts .parts-header {{
  3441. background: #fed7aa;
  3442. color: #92400e;
  3443. }}
  3444. .parts-content {{
  3445. padding: 8px 12px;
  3446. }}
  3447. .part-item {{
  3448. padding: 6px 0;
  3449. border-bottom: 1px solid rgba(0,0,0,0.05);
  3450. font-size: 13px;
  3451. line-height: 1.6;
  3452. }}
  3453. .part-item:last-child {{
  3454. border-bottom: none;
  3455. }}
  3456. .part-key {{
  3457. font-weight: 600;
  3458. color: #374151;
  3459. margin-right: 6px;
  3460. }}
  3461. .part-value {{
  3462. color: #1f2937;
  3463. }}
  3464. .increment-context {{
  3465. background: #fef3c7;
  3466. padding: 10px 12px;
  3467. border-radius: 6px;
  3468. margin: 10px 0;
  3469. font-size: 12px;
  3470. color: #92400e;
  3471. line-height: 1.6;
  3472. border-left: 3px solid #f59e0b;
  3473. }}
  3474. @media (max-width: 768px) {{
  3475. .main-content-layout {{
  3476. flex-direction: column;
  3477. }}
  3478. .sidebar-nav {{
  3479. position: relative;
  3480. width: 100%;
  3481. max-height: 300px;
  3482. }}
  3483. .inspirations-grid {{
  3484. grid-template-columns: 1fr;
  3485. }}
  3486. .header h1 {{
  3487. font-size: 32px;
  3488. }}
  3489. .stats-overview {{
  3490. grid-template-columns: repeat(2, 1fr);
  3491. }}
  3492. }}
  3493. </style>
  3494. </head>
  3495. <body>
  3496. <div class="container">
  3497. <div class="main-content-layout">
  3498. <div class="sidebar-nav">
  3499. <div class="nav-title">📑 灵感点列表</div>
  3500. <div class="nav-list" id="navList">
  3501. <!-- 目录将通过JS动态生成 -->
  3502. </div>
  3503. </div>
  3504. <div class="inspirations-section">
  3505. <div class="inspiration-display">
  3506. {cards_html_str}
  3507. </div>
  3508. </div>
  3509. </div>
  3510. <!-- Modal -->
  3511. <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
  3512. <div class="modal-content">
  3513. <button class="modal-close" onclick="closeModal()">&times;</button>
  3514. <div class="modal-body" id="modalBody">
  3515. <!-- Content will be inserted here -->
  3516. </div>
  3517. </div>
  3518. </div>
  3519. <!-- Score Detail Modal -->
  3520. <div id="scoreDetailModal" class="modal-overlay" onclick="closeScoreDetailModal(event)">
  3521. <div class="modal-content score-detail-content">
  3522. <button class="modal-close" onclick="closeScoreDetail()">&times;</button>
  3523. <div class="modal-body" id="scoreModalBody">
  3524. <!-- Score detail content will be inserted here -->
  3525. </div>
  3526. </div>
  3527. </div>
  3528. </div>
  3529. <script>
  3530. {detail_modal_js}
  3531. </script>
  3532. </body>
  3533. </html>'''
  3534. # 写入文件
  3535. output_file = Path(output_path)
  3536. output_file.parent.mkdir(parents=True, exist_ok=True)
  3537. with open(output_file, 'w', encoding='utf-8') as f:
  3538. f.write(html_content)
  3539. return str(output_file.absolute())
  3540. def load_persona_data(persona_path: str) -> Dict[str, Any]:
  3541. """
  3542. 加载人设数据
  3543. Args:
  3544. persona_path: 人设JSON文件路径
  3545. Returns:
  3546. 人设数据字典
  3547. """
  3548. try:
  3549. with open(persona_path, 'r', encoding='utf-8') as f:
  3550. return json.load(f)
  3551. except Exception as e:
  3552. print(f"警告: 读取人设文件失败: {e}")
  3553. return {}
  3554. def main():
  3555. """主函数"""
  3556. import sys
  3557. import os
  3558. # 配置路径(使用当前脚本的相对路径)
  3559. script_dir = os.path.dirname(os.path.abspath(__file__))
  3560. base_dir = os.path.join(script_dir, "data/阿里多多酱")
  3561. inspiration_dir = os.path.join(base_dir, "out/人设_1110/how/灵感点")
  3562. posts_dir = os.path.join(base_dir, "作者历史帖子")
  3563. persona_path = os.path.join(base_dir, "out/人设_1110/人设.json")
  3564. inspiration_to_post_path = os.path.join(base_dir, "out/人设_1110/点到帖子映射.json")
  3565. category_index_path = os.path.join(base_dir, "out/人设_1110/分类索引_完整.json")
  3566. post_to_mapping_path = os.path.join(base_dir, "out/人设_1110/帖子到分类和点映射.json")
  3567. output_path = os.path.join(base_dir, "out/人设_1110/how/灵感点可视化.html")
  3568. print("=" * 60)
  3569. print("灵感点分析可视化脚本")
  3570. print("=" * 60)
  3571. # 加载数据
  3572. print("\n📂 正在加载灵感点数据...")
  3573. inspirations_data = load_inspiration_points_data(inspiration_dir)
  3574. print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
  3575. print("\n📂 正在加载帖子数据...")
  3576. posts_map = load_posts_data(posts_dir)
  3577. print(f"✅ 成功加载 {len(posts_map)} 个帖子")
  3578. print("\n📂 正在加载人设数据...")
  3579. persona_data = load_persona_data(persona_path)
  3580. print(f"✅ 成功加载人设数据")
  3581. print("\n📂 正在加载点到帖子映射数据...")
  3582. with open(inspiration_to_post_path, 'r', encoding='utf-8') as f:
  3583. inspiration_to_post_data = json.load(f)
  3584. print(f"✅ 成功加载点到帖子映射数据")
  3585. print("\n📂 正在加载分类索引数据...")
  3586. with open(category_index_path, 'r', encoding='utf-8') as f:
  3587. category_index_data = json.load(f)
  3588. print(f"✅ 成功加载分类索引数据")
  3589. print("\n📂 正在加载帖子到分类和点映射数据...")
  3590. with open(post_to_mapping_path, 'r', encoding='utf-8') as f:
  3591. post_to_mapping_data = json.load(f)
  3592. print(f"✅ 成功加载帖子到分类和点映射数据")
  3593. # 生成HTML
  3594. print("\n🎨 正在生成可视化HTML...")
  3595. result_path = generate_html(
  3596. inspirations_data,
  3597. posts_map,
  3598. persona_data,
  3599. output_path,
  3600. inspiration_to_post_data,
  3601. category_index_data,
  3602. post_to_mapping_data
  3603. )
  3604. print(f"\n✅ 可视化文件已生成!")
  3605. print(f"📄 文件路径: {result_path}")
  3606. print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
  3607. print("=" * 60)
  3608. if __name__ == "__main__":
  3609. main()