visualize_inspiration_points.py 133 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162
  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. </div>
  760. </div>
  761. <div class="match-toggle-main">▼</div>
  762. </div>
  763. <div class="match-main-content">
  764. {match_info_section}
  765. {step1_html}
  766. {step2_html}
  767. </div>
  768. </div>
  769. '''
  770. # 获取top1匹配的灵感分类名称(用于顶部标题)
  771. top1_category_name = ""
  772. if step1:
  773. step1_matches = step1.get("匹配结果列表", [])
  774. if step1_matches:
  775. top_match = step1_matches[0]
  776. input_info = top_match.get("输入信息", {})
  777. top1_category_name = input_info.get("A", "")
  778. html = f'''
  779. <div class="inspiration-detail"
  780. data-inspiration-name="{inspiration_name_escaped}"
  781. data-step1-score="{step1_score}"
  782. data-top3-matches="{top3_json_escaped}">
  783. <div class="breadcrumb-container">
  784. <div class="breadcrumb" id="dynamicBreadcrumb">
  785. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> {inspiration_name_escaped}</span>
  786. </div>
  787. </div>
  788. <div class="inspiration-content-wrapper">
  789. <div class="matches-list">
  790. {matches_html}
  791. </div>
  792. </div>
  793. </div>
  794. '''
  795. return html
  796. def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
  797. """
  798. 生成灵感点的详细信息HTML
  799. Args:
  800. inspiration_data: 灵感点数据
  801. Returns:
  802. 详细信息的HTML字符串
  803. """
  804. import html as html_module
  805. summary = inspiration_data.get("summary", {})
  806. step1 = inspiration_data.get("step1", {})
  807. step2 = inspiration_data.get("step2", {})
  808. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  809. content = f'''
  810. <div class="modal-header">
  811. <h2 class="modal-title">{html_module.escape(inspiration_name)}</h2>
  812. </div>
  813. '''
  814. # 获取元数据,用于后面的日志链接
  815. metadata = summary.get("元数据", {})
  816. # Step1 详细信息
  817. if step1 and step1.get("灵感"):
  818. inspiration = step1.get("灵感", "")
  819. matches = step1.get("匹配结果列表", [])
  820. content += f'''
  821. <div class="modal-section">
  822. <h3>🎯 Step1: 灵感人设匹配</h3>
  823. <div class="step-content">
  824. <div class="step-field">
  825. <span class="step-field-label">灵感内容:</span>
  826. <span class="step-field-value">{html_module.escape(inspiration)}</span>
  827. </div>
  828. '''
  829. # 显示匹配结果(只显示Top1)
  830. if matches:
  831. content += f'''
  832. <div class="step-field">
  833. <span class="step-field-label">Top1匹配结果:</span>
  834. <div class="matches-list">
  835. '''
  836. for index, match in enumerate(matches[:1]):
  837. input_info = match.get("输入信息", {})
  838. match_result = match.get("匹配结果", {})
  839. element_a = input_info.get("A", "")
  840. context_a = input_info.get("A_Context", "")
  841. score = match_result.get("score", 0)
  842. score_explain = match_result.get("score说明", "") or ""
  843. same_parts = match_result.get("相同部分", {}) or {}
  844. increment_parts = match_result.get("增量部分", {}) or {}
  845. content += f'''
  846. <div class="match-item">
  847. <div class="match-header">
  848. <span class="match-element-name">{html_module.escape(element_a)}</span>
  849. <span class="match-score">{score:.2f}</span>
  850. </div>
  851. '''
  852. if context_a:
  853. content += f'<div class="match-context"><strong>📍 所属分类:</strong> {html_module.escape(context_a).replace(chr(10), "<br>")}</div>'
  854. if score_explain:
  855. content += f'<div class="match-explain"><strong>💡 分数说明:</strong> {html_module.escape(score_explain)}</div>'
  856. # 相同部分
  857. if same_parts:
  858. content += '''
  859. <div class="match-parts same-parts">
  860. <div class="parts-header">✅ 相同部分</div>
  861. <div class="parts-content">
  862. '''
  863. for key, value in same_parts.items():
  864. content += f'''
  865. <div class="part-item">
  866. <span class="part-key">{html_module.escape(key)}:</span>
  867. <span class="part-value">{html_module.escape(value)}</span>
  868. </div>
  869. '''
  870. content += '''
  871. </div>
  872. </div>
  873. '''
  874. # 增量部分
  875. if increment_parts:
  876. content += '''
  877. <div class="match-parts increment-parts">
  878. <div class="parts-header">➕ 增量部分</div>
  879. <div class="parts-content">
  880. '''
  881. for key, value in increment_parts.items():
  882. content += f'''
  883. <div class="part-item">
  884. <span class="part-key">{html_module.escape(key)}:</span>
  885. <span class="part-value">{html_module.escape(value)}</span>
  886. </div>
  887. '''
  888. content += '''
  889. </div>
  890. </div>
  891. '''
  892. content += '''
  893. </div>
  894. '''
  895. content += '''
  896. </div>
  897. </div>
  898. '''
  899. content += '''
  900. </div>
  901. </div>
  902. '''
  903. # 日志链接
  904. if metadata.get("log_url"):
  905. content += f'''
  906. <div class="modal-link">
  907. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  908. 🔗 查看详细日志
  909. </a>
  910. </div>
  911. '''
  912. return content
  913. def generate_detail_modal_content_js() -> str:
  914. """
  915. 生成详情弹窗内容的JavaScript函数
  916. Returns:
  917. JavaScript代码字符串
  918. """
  919. return '''
  920. // 笔记图片当前索引管理
  921. const noteImageStates = {};
  922. // 移动笔记图片
  923. function moveNoteImage(noteId, direction) {
  924. if (!noteImageStates[noteId]) {
  925. noteImageStates[noteId] = 0;
  926. }
  927. const carousel = document.querySelector(`[data-note-id="${noteId}"]`);
  928. if (!carousel) return;
  929. const totalImages = parseInt(carousel.dataset.totalImages);
  930. const track = document.getElementById(noteId + '-track');
  931. if (!track) return;
  932. let newIndex = noteImageStates[noteId] + direction;
  933. if (newIndex < 0) newIndex = 0;
  934. if (newIndex >= totalImages) newIndex = totalImages - 1;
  935. noteImageStates[noteId] = newIndex;
  936. // 移动轨道
  937. track.style.transform = `translateX(-${newIndex * 100}%)`;
  938. // 更新指示点
  939. const dots = document.querySelectorAll(`#${noteId}-dots .note-image-dot`);
  940. dots.forEach((dot, i) => {
  941. dot.classList.toggle('active', i === newIndex);
  942. });
  943. // 更新按钮状态
  944. const prevBtn = carousel.querySelector('.note-carousel-button.prev');
  945. const nextBtn = carousel.querySelector('.note-carousel-button.next');
  946. if (prevBtn) {
  947. prevBtn.classList.toggle('disabled', newIndex === 0);
  948. }
  949. if (nextBtn) {
  950. nextBtn.classList.toggle('disabled', newIndex >= totalImages - 1);
  951. }
  952. }
  953. // 显示笔记详情
  954. function showNoteDetail(element) {
  955. const noteDataStr = element.dataset.noteData;
  956. if (!noteDataStr) return;
  957. try {
  958. const noteData = JSON.parse(noteDataStr);
  959. // 生成图片HTML
  960. let imagesHtml = '';
  961. if (noteData.images && noteData.images.length > 0) {
  962. imagesHtml = noteData.images.map(img =>
  963. `<img src="${img}" class="note-detail-image" alt="图片">`
  964. ).join('');
  965. } else {
  966. imagesHtml = '<div style="text-align: center; color: #9ca3af; padding: 40px;">暂无图片</div>';
  967. }
  968. // 生成灵感点、关键点、目的点HTML
  969. let pointsDetailHtml = '';
  970. const hasPoints = (noteData.inspiration_points && noteData.inspiration_points.length > 0) ||
  971. (noteData.key_points && noteData.key_points.length > 0) ||
  972. (noteData.purpose_points && noteData.purpose_points.length > 0);
  973. if (hasPoints) {
  974. let sections = [];
  975. // 灵感点
  976. if (noteData.inspiration_points && noteData.inspiration_points.length > 0) {
  977. const items = noteData.inspiration_points.map(p =>
  978. `<div class="detail-point-item">
  979. <div class="detail-point-name">${p.灵感点 || ''}</div>
  980. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  981. </div>`
  982. ).join('');
  983. sections.push(`<div class="detail-points-section">
  984. <div class="detail-points-title">💡 灵感点</div>
  985. ${items}
  986. </div>`);
  987. }
  988. // 关键点
  989. if (noteData.key_points && noteData.key_points.length > 0) {
  990. const items = noteData.key_points.map(k =>
  991. `<div class="detail-point-item">
  992. <div class="detail-point-name">${k.关键点 || ''}</div>
  993. ${k.描述 ? `<div class="detail-point-desc">${k.描述}</div>` : ''}
  994. </div>`
  995. ).join('');
  996. sections.push(`<div class="detail-points-section">
  997. <div class="detail-points-title">🔑 关键点</div>
  998. ${items}
  999. </div>`);
  1000. }
  1001. // 目的点
  1002. if (noteData.purpose_points && noteData.purpose_points.length > 0) {
  1003. const items = noteData.purpose_points.map(p =>
  1004. `<div class="detail-point-item">
  1005. <div class="detail-point-name">${p.目的点 || ''}</div>
  1006. ${p.描述 ? `<div class="detail-point-desc">${p.描述}</div>` : ''}
  1007. </div>`
  1008. ).join('');
  1009. sections.push(`<div class="detail-points-section">
  1010. <div class="detail-points-title">🎯 目的点</div>
  1011. ${items}
  1012. </div>`);
  1013. }
  1014. pointsDetailHtml = `<div class="note-detail-points">${sections.join('')}</div>`;
  1015. }
  1016. const modalHtml = `
  1017. <div class="note-detail-content">
  1018. <button class="note-detail-close" onclick="closeNoteDetail()">×</button>
  1019. <div class="note-detail-header">
  1020. <div class="note-detail-title">${noteData.title || '无标题'}</div>
  1021. <div class="note-detail-meta">
  1022. <span class="note-detail-author">@${noteData.author}</span>
  1023. <div class="note-detail-stats">
  1024. <span>👍 ${noteData.like_count}</span>
  1025. <span>💬 ${noteData.comment_count}</span>
  1026. </div>
  1027. </div>
  1028. </div>
  1029. <div class="note-detail-body">
  1030. ${noteData.desc ? `<div class="note-detail-desc">${noteData.desc}</div>` : ''}
  1031. ${pointsDetailHtml}
  1032. <div class="note-detail-images">
  1033. ${imagesHtml}
  1034. </div>
  1035. </div>
  1036. <div class="note-detail-footer">
  1037. <a href="${noteData.link}" target="_blank" class="note-detail-link">
  1038. 在小红书查看完整内容 →
  1039. </a>
  1040. </div>
  1041. </div>
  1042. `;
  1043. let modal = document.getElementById('noteDetailModal');
  1044. if (!modal) {
  1045. modal = document.createElement('div');
  1046. modal.id = 'noteDetailModal';
  1047. modal.className = 'note-detail-modal';
  1048. modal.onclick = (e) => {
  1049. if (e.target === modal) closeNoteDetail();
  1050. };
  1051. document.body.appendChild(modal);
  1052. }
  1053. modal.innerHTML = modalHtml;
  1054. modal.classList.add('active');
  1055. document.body.style.overflow = 'hidden';
  1056. } catch (e) {
  1057. console.error('Error parsing note data:', e);
  1058. }
  1059. }
  1060. // 关闭笔记详情
  1061. function closeNoteDetail() {
  1062. const modal = document.getElementById('noteDetailModal');
  1063. if (modal) {
  1064. modal.classList.remove('active');
  1065. document.body.style.overflow = '';
  1066. }
  1067. }
  1068. // 显示分数详情
  1069. function showScoreDetail(element) {
  1070. const scoreDetailStr = element.dataset.scoreDetail;
  1071. if (!scoreDetailStr) return;
  1072. try {
  1073. const scoreData = JSON.parse(scoreDetailStr);
  1074. const modal = document.getElementById('scoreDetailModal');
  1075. const modalBody = document.getElementById('scoreModalBody');
  1076. // 生成相同部分HTML
  1077. let samePartsHTML = '';
  1078. if (scoreData.相同部分 && Object.keys(scoreData.相同部分).length > 0) {
  1079. const sameItems = Object.entries(scoreData.相同部分).map(([key, value]) =>
  1080. `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
  1081. ).join('');
  1082. samePartsHTML = `
  1083. <div class="score-parts same-parts">
  1084. <div class="score-parts-title">✅ 相同部分</div>
  1085. ${sameItems}
  1086. </div>
  1087. `;
  1088. }
  1089. // 生成增量部分HTML
  1090. let incrementPartsHTML = '';
  1091. if (scoreData.增量部分 && Object.keys(scoreData.增量部分).length > 0) {
  1092. const incItems = Object.entries(scoreData.增量部分).map(([key, value]) =>
  1093. `<div class="score-part-item"><span class="score-part-key">${key}:</span><span class="score-part-value">${value}</span></div>`
  1094. ).join('');
  1095. incrementPartsHTML = `
  1096. <div class="score-parts increment-parts">
  1097. <div class="score-parts-title">➕ 增量部分</div>
  1098. ${incItems}
  1099. </div>
  1100. `;
  1101. }
  1102. // 生成分数说明HTML
  1103. let explainHTML = '';
  1104. if (scoreData.score说明) {
  1105. explainHTML = `
  1106. <div class="score-detail-explain">
  1107. <div class="score-explain-title">💡 分数说明</div>
  1108. <div class="score-explain-text">${scoreData.score说明}</div>
  1109. </div>
  1110. `;
  1111. }
  1112. // 生成输入信息HTML
  1113. let inputInfoHTML = '';
  1114. if (scoreData.输入B || scoreData.输入A) {
  1115. inputInfoHTML = `
  1116. <div class="score-input-info">
  1117. <div class="score-input-title">📝 输入信息</div>
  1118. ${scoreData.输入B ? `
  1119. <div class="score-input-item">
  1120. <div class="score-input-label">输入B(灵感点):</div>
  1121. <div class="score-input-value">${scoreData.输入B}</div>
  1122. </div>
  1123. ` : ''}
  1124. ${scoreData.B_Context ? `
  1125. <div class="score-input-item">
  1126. <div class="score-input-label">B_Context:</div>
  1127. <div class="score-input-value">${scoreData.B_Context}</div>
  1128. </div>
  1129. ` : ''}
  1130. ${scoreData.输入A ? `
  1131. <div class="score-input-item">
  1132. <div class="score-input-label">输入A(帖子标题):</div>
  1133. <div class="score-input-value">${scoreData.输入A}</div>
  1134. </div>
  1135. ` : ''}
  1136. ${scoreData.A_Context ? `
  1137. <div class="score-input-item">
  1138. <div class="score-input-label">A_Context(帖子内容摘要):</div>
  1139. <div class="score-input-value">${scoreData.A_Context}</div>
  1140. </div>
  1141. ` : ''}
  1142. </div>
  1143. `;
  1144. }
  1145. modalBody.innerHTML = `
  1146. <div class="score-detail-container">
  1147. <h2 class="score-detail-title">匹配分数详情</h2>
  1148. <div class="score-detail-score">
  1149. <span class="score-detail-label">匹配分数:</span>
  1150. <span class="score-detail-value">${scoreData.score.toFixed(2)}</span>
  1151. </div>
  1152. ${inputInfoHTML}
  1153. ${explainHTML}
  1154. <div class="score-parts-container">
  1155. ${samePartsHTML}
  1156. ${incrementPartsHTML}
  1157. </div>
  1158. </div>
  1159. `;
  1160. modal.classList.add('active');
  1161. document.body.style.overflow = 'hidden';
  1162. } catch (e) {
  1163. console.error('Failed to parse score detail:', e);
  1164. }
  1165. }
  1166. // 关闭分数详情
  1167. function closeScoreDetail() {
  1168. const modal = document.getElementById('scoreDetailModal');
  1169. if (modal) {
  1170. modal.classList.remove('active');
  1171. document.body.style.overflow = '';
  1172. }
  1173. }
  1174. // 点击Modal背景关闭分数详情
  1175. function closeScoreDetailModal(event) {
  1176. if (event.target.id === 'scoreDetailModal') {
  1177. closeScoreDetail();
  1178. }
  1179. }
  1180. // 搜索结果排序
  1181. function sortSearchResults(button, containerId) {
  1182. const sortType = button.dataset.sort;
  1183. const container = document.getElementById(containerId);
  1184. if (!container) return;
  1185. // 更新按钮状态
  1186. const allButtons = button.parentElement.querySelectorAll('.search-sort-btn');
  1187. allButtons.forEach(btn => btn.classList.remove('active'));
  1188. button.classList.add('active');
  1189. // 获取所有搜索结果卡片
  1190. const items = Array.from(container.querySelectorAll('.search-note-item'));
  1191. // 根据排序类型排序
  1192. items.sort((a, b) => {
  1193. if (sortType === 'score') {
  1194. const scoreA = parseFloat(a.dataset.score) || -1;
  1195. const scoreB = parseFloat(b.dataset.score) || -1;
  1196. return scoreB - scoreA; // 降序
  1197. } else if (sortType === 'original') {
  1198. const indexA = parseInt(a.dataset.originalIndex) || 0;
  1199. const indexB = parseInt(b.dataset.originalIndex) || 0;
  1200. return indexA - indexB; // 升序
  1201. } else if (sortType === 'likes') {
  1202. const likesA = parseInt(a.dataset.likes) || 0;
  1203. const likesB = parseInt(b.dataset.likes) || 0;
  1204. return likesB - likesA; // 降序
  1205. }
  1206. return 0;
  1207. });
  1208. // 重新排列DOM
  1209. items.forEach(item => container.appendChild(item));
  1210. }
  1211. // ESC键关闭详情
  1212. document.addEventListener('keydown', function(event) {
  1213. if (event.key === 'Escape') {
  1214. closeNoteDetail();
  1215. closeScoreDetail();
  1216. }
  1217. });
  1218. // 切换主匹配项的展开/折叠
  1219. function toggleMainMatch(element) {
  1220. const matchItem = element.closest('.match-item');
  1221. matchItem.classList.toggle('expanded');
  1222. }
  1223. // 切换步骤wrapper的展开/折叠
  1224. function toggleStepWrapper(element) {
  1225. const stepWrapper = element.closest('.step-section-wrapper');
  1226. stepWrapper.classList.toggle('expanded');
  1227. }
  1228. // 切换步骤的展开/折叠
  1229. function toggleStep(element) {
  1230. const stepSection = element.closest('.step-section');
  1231. stepSection.classList.toggle('expanded');
  1232. }
  1233. // 切换匹配详情的展开/折叠
  1234. function toggleMatchSection(element) {
  1235. const matchSection = element.closest('.match-section');
  1236. matchSection.classList.toggle('expanded');
  1237. }
  1238. // 显示指定的灵感详情
  1239. function showDetail(index) {
  1240. const details = document.querySelectorAll('.inspiration-detail');
  1241. details.forEach((detail, i) => {
  1242. if (i === index) {
  1243. detail.classList.add('active');
  1244. // 滚动到顶部
  1245. const section = document.querySelector('.inspirations-section');
  1246. if (section) {
  1247. section.scrollTop = 0;
  1248. }
  1249. } else {
  1250. detail.classList.remove('active');
  1251. }
  1252. });
  1253. }
  1254. // 生成导航目录
  1255. function generateNavigation() {
  1256. const details = document.querySelectorAll('.inspiration-detail');
  1257. const navList = document.getElementById('navList');
  1258. navList.innerHTML = '';
  1259. details.forEach((detail, index) => {
  1260. const name = detail.dataset.inspirationName;
  1261. const score = parseFloat(detail.dataset.step1Score) || 0;
  1262. const top3MatchesStr = detail.dataset.top3Matches;
  1263. let top3Matches = [];
  1264. try {
  1265. top3Matches = JSON.parse(top3MatchesStr);
  1266. } catch(e) {
  1267. console.error('Error parsing top3 matches:', e);
  1268. }
  1269. const navItem = document.createElement('div');
  1270. navItem.className = 'nav-item';
  1271. if (index === 0) navItem.classList.add('active');
  1272. navItem.dataset.cardIndex = index;
  1273. // 生成匹配列表HTML - 精简版:只显示[灵感分类]名称 score
  1274. let matchesHtml = '';
  1275. if (top3Matches && top3Matches.length > 0) {
  1276. matchesHtml = '<div class="nav-item-matches">';
  1277. top3Matches.forEach((match, i) => {
  1278. matchesHtml += `
  1279. <div class="nav-match-item-compact">
  1280. <span class="nav-match-label">[灵感分类]</span>
  1281. <span class="nav-match-name-compact">${match.name}</span>
  1282. <span class="nav-match-score-compact">${match.score.toFixed(2)}</span>
  1283. </div>
  1284. `;
  1285. });
  1286. matchesHtml += '</div>';
  1287. }
  1288. navItem.innerHTML = `
  1289. <div class="nav-item-header">
  1290. <span class="nav-item-name" title="${name}">
  1291. <span class="nav-label">[灵感点]</span>${name}
  1292. </span>
  1293. <span class="nav-item-score">${score.toFixed(2)}</span>
  1294. </div>
  1295. ${matchesHtml}
  1296. `;
  1297. navItem.addEventListener('click', () => {
  1298. // 移除所有active状态
  1299. document.querySelectorAll('.nav-item').forEach(item => {
  1300. item.classList.remove('active');
  1301. });
  1302. // 添加当前active状态
  1303. navItem.classList.add('active');
  1304. // 显示对应详情
  1305. showDetail(index);
  1306. });
  1307. navList.appendChild(navItem);
  1308. });
  1309. // 默认显示第一个详情
  1310. if (details.length > 0) {
  1311. showDetail(0);
  1312. }
  1313. }
  1314. // 更新面包屑
  1315. function updateBreadcrumb(matchName, stepName) {
  1316. const breadcrumb = document.querySelector('.inspiration-detail.active #dynamicBreadcrumb');
  1317. if (!breadcrumb) return;
  1318. const inspirationName = document.querySelector('.inspiration-detail.active').dataset.inspirationName;
  1319. let breadcrumbHtml = `
  1320. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感点]</span> ${inspirationName}</span>
  1321. `;
  1322. if (matchName) {
  1323. breadcrumbHtml += `
  1324. <span class="breadcrumb-separator">›</span>
  1325. <span class="breadcrumb-item"><span class="breadcrumb-label">[灵感分类]</span> ${matchName}</span>
  1326. `;
  1327. }
  1328. if (stepName) {
  1329. breadcrumbHtml += `
  1330. <span class="breadcrumb-separator">›</span>
  1331. <span class="breadcrumb-item breadcrumb-current">${stepName}</span>
  1332. `;
  1333. }
  1334. breadcrumb.innerHTML = breadcrumbHtml;
  1335. }
  1336. // 监听滚动,更新面包屑
  1337. function setupBreadcrumbObserver() {
  1338. const activeDetail = document.querySelector('.inspiration-detail.active');
  1339. if (!activeDetail) return;
  1340. const contentWrapper = activeDetail.querySelector('.inspiration-content-wrapper');
  1341. if (!contentWrapper) return;
  1342. // 获取所有需要监听的section
  1343. const sections = activeDetail.querySelectorAll('.step-section');
  1344. if (sections.length === 0) return;
  1345. // 创建Intersection Observer
  1346. const observerOptions = {
  1347. root: null,
  1348. rootMargin: '-100px 0px -50% 0px',
  1349. threshold: 0
  1350. };
  1351. const observer = new IntersectionObserver((entries) => {
  1352. entries.forEach(entry => {
  1353. if (entry.isIntersecting) {
  1354. const section = entry.target;
  1355. const matchItem = section.closest('.match-item');
  1356. const matchName = matchItem ? matchItem.dataset.matchName : '';
  1357. const stepName = section.dataset.stepName || '';
  1358. updateBreadcrumb(matchName, stepName);
  1359. }
  1360. });
  1361. }, observerOptions);
  1362. // 观察所有section
  1363. sections.forEach(section => observer.observe(section));
  1364. // 存储observer以便清理
  1365. if (!window.breadcrumbObservers) {
  1366. window.breadcrumbObservers = [];
  1367. }
  1368. window.breadcrumbObservers.push(observer);
  1369. }
  1370. // 页面加载时生成导航
  1371. document.addEventListener('DOMContentLoaded', () => {
  1372. generateNavigation();
  1373. setupBreadcrumbObserver();
  1374. });
  1375. // 当切换灵感点时,重新设置observer
  1376. const originalShowDetail = showDetail;
  1377. showDetail = function(index) {
  1378. // 清理旧的observers
  1379. if (window.breadcrumbObservers) {
  1380. window.breadcrumbObservers.forEach(obs => obs.disconnect());
  1381. window.breadcrumbObservers = [];
  1382. }
  1383. // 调用原始函数
  1384. originalShowDetail(index);
  1385. // 设置新的observer
  1386. setTimeout(() => {
  1387. setupBreadcrumbObserver();
  1388. }, 100);
  1389. };
  1390. '''
  1391. def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
  1392. """
  1393. 生成人设结构的树状HTML
  1394. Args:
  1395. persona_data: 人设数据
  1396. Returns:
  1397. 人设结构的HTML字符串
  1398. """
  1399. if not persona_data:
  1400. return '<div class="empty-state">暂无人设数据</div>'
  1401. inspiration_list = persona_data.get("灵感点列表", [])
  1402. if not inspiration_list:
  1403. return '<div class="empty-state">暂无灵感点列表数据</div>'
  1404. html_parts = ['<div class="tree">']
  1405. for perspective_idx, perspective in enumerate(inspiration_list):
  1406. perspective_name = perspective.get("视角名称", "未知视角")
  1407. perspective_desc = perspective.get("视角描述", "")
  1408. pattern_list = perspective.get("模式列表", [])
  1409. # 一级节点:视角
  1410. html_parts.append(f'''
  1411. <ul>
  1412. <li>
  1413. <div class="tree-node level-1">
  1414. <span class="node-icon">📁</span>
  1415. <span class="node-name">{html_module.escape(perspective_name)}</span>
  1416. <span class="node-count">{len(pattern_list)}个分类</span>
  1417. </div>
  1418. ''')
  1419. if perspective_desc:
  1420. html_parts.append(f'''
  1421. <div class="node-desc">{html_module.escape(perspective_desc)}</div>
  1422. ''')
  1423. # 二级节点:分类
  1424. if pattern_list:
  1425. html_parts.append('<ul>')
  1426. for pattern in pattern_list:
  1427. category_name = pattern.get("分类名称", "未知分类")
  1428. core_definition = pattern.get("核心定义", "")
  1429. subcategories = pattern.get("二级细分", [])
  1430. total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
  1431. html_parts.append(f'''
  1432. <li>
  1433. <div class="tree-node level-2">
  1434. <span class="node-icon">📂</span>
  1435. <span class="node-name">{html_module.escape(category_name)}</span>
  1436. <span class="node-count">{total_posts}个帖子</span>
  1437. </div>
  1438. ''')
  1439. if core_definition:
  1440. html_parts.append(f'''
  1441. <div class="node-desc">{html_module.escape(core_definition)}</div>
  1442. ''')
  1443. # 三级节点:细分
  1444. if subcategories:
  1445. html_parts.append('<ul>')
  1446. for subcategory in subcategories:
  1447. sub_name = subcategory.get("分类名称", "未知细分")
  1448. sub_definition = subcategory.get("分类定义", "")
  1449. post_ids = subcategory.get("帖子ID列表", [])
  1450. html_parts.append(f'''
  1451. <li>
  1452. <div class="tree-node level-3">
  1453. <span class="node-icon">📄</span>
  1454. <span class="node-name">{html_module.escape(sub_name)}</span>
  1455. <span class="node-count">{len(post_ids)}个帖子</span>
  1456. </div>
  1457. ''')
  1458. if sub_definition:
  1459. html_parts.append(f'''
  1460. <div class="node-desc">{html_module.escape(sub_definition)}</div>
  1461. ''')
  1462. if post_ids:
  1463. html_parts.append(f'''
  1464. <div class="node-posts">
  1465. <span class="posts-label">📋 帖子ID:</span>
  1466. <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
  1467. {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
  1468. </div>
  1469. ''')
  1470. html_parts.append('</li>')
  1471. html_parts.append('</ul>')
  1472. html_parts.append('</li>')
  1473. html_parts.append('</ul>')
  1474. html_parts.append('</li>')
  1475. html_parts.append('</ul>')
  1476. html_parts.append('</div>')
  1477. return ''.join(html_parts)
  1478. def generate_html(
  1479. inspirations_data: List[Dict[str, Any]],
  1480. posts_map: Dict[str, Dict[str, Any]],
  1481. persona_data: Dict[str, Any],
  1482. output_path: str,
  1483. inspiration_to_post_data: Dict[str, Any] = None,
  1484. category_index_data: Dict[str, Any] = None,
  1485. post_to_mapping_data: Dict[str, Any] = None
  1486. ) -> str:
  1487. """
  1488. 生成完整的可视化HTML
  1489. Args:
  1490. inspirations_data: 灵感点数据列表
  1491. posts_map: 帖子数据映射
  1492. persona_data: 人设数据
  1493. output_path: 输出文件路径
  1494. Returns:
  1495. 输出文件路径
  1496. """
  1497. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  1498. # 统计信息
  1499. total_count = len(inspirations_data)
  1500. # Step1 统计
  1501. step1_excellent_count = sum(1 for d in inspirations_data
  1502. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7)
  1503. step1_good_count = sum(1 for d in inspirations_data
  1504. if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7)
  1505. step1_normal_count = sum(1 for d in inspirations_data
  1506. if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5)
  1507. step1_need_opt_count = sum(1 for d in inspirations_data
  1508. if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3)
  1509. # 平均分数
  1510. total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0)
  1511. for d in inspirations_data)
  1512. avg_step1_score = total_step1_score / total_count if total_count > 0 else 0
  1513. # 按Step1分数排序
  1514. inspirations_data_sorted = sorted(
  1515. inspirations_data,
  1516. key=lambda x: x["summary"].get("关键指标", {}).get("step1_top1_score", 0),
  1517. reverse=True
  1518. )
  1519. # 生成卡片HTML
  1520. cards_html = [
  1521. generate_inspiration_card_html(data, inspiration_to_post_data, category_index_data, post_to_mapping_data)
  1522. for data in inspirations_data_sorted
  1523. ]
  1524. cards_html_str = '\n'.join(cards_html)
  1525. # 生成人设结构HTML
  1526. persona_structure_html = generate_persona_structure_html(persona_data)
  1527. # 生成JavaScript
  1528. detail_modal_js = generate_detail_modal_content_js()
  1529. # 完整HTML
  1530. html_content = f'''<!DOCTYPE html>
  1531. <html lang="zh-CN">
  1532. <head>
  1533. <meta charset="UTF-8">
  1534. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1535. <title>灵感点分析可视化</title>
  1536. <style>
  1537. * {{
  1538. margin: 0;
  1539. padding: 0;
  1540. box-sizing: border-box;
  1541. }}
  1542. body {{
  1543. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  1544. background: #f5f7fa;
  1545. color: #333;
  1546. line-height: 1.6;
  1547. margin: 0;
  1548. padding: 0;
  1549. overflow: hidden;
  1550. }}
  1551. .container {{
  1552. max-width: 100%;
  1553. height: 100vh;
  1554. margin: 0;
  1555. padding: 0;
  1556. display: flex;
  1557. }}
  1558. .main-content-layout {{
  1559. display: flex;
  1560. width: 100%;
  1561. height: 100vh;
  1562. overflow: hidden;
  1563. }}
  1564. .sidebar-nav {{
  1565. width: 400px;
  1566. background: white;
  1567. padding: 30px 20px;
  1568. box-shadow: 2px 0 10px rgba(0,0,0,0.1);
  1569. overflow-y: auto;
  1570. flex-shrink: 0;
  1571. height: 100vh;
  1572. }}
  1573. .nav-title {{
  1574. font-size: 18px;
  1575. font-weight: 700;
  1576. color: #1a1a1a;
  1577. margin-bottom: 15px;
  1578. padding-bottom: 10px;
  1579. border-bottom: 2px solid #e5e7eb;
  1580. }}
  1581. .nav-list {{
  1582. display: flex;
  1583. flex-direction: column;
  1584. gap: 12px;
  1585. }}
  1586. .nav-item {{
  1587. border-radius: 10px;
  1588. cursor: pointer;
  1589. transition: all 0.3s;
  1590. border: 2px solid #e5e7eb;
  1591. overflow: hidden;
  1592. background: white;
  1593. }}
  1594. .nav-item:hover {{
  1595. border-color: #667eea;
  1596. box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
  1597. }}
  1598. .nav-item.active {{
  1599. border-color: #667eea;
  1600. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.25);
  1601. }}
  1602. .nav-item-header {{
  1603. padding: 12px 14px;
  1604. background: #f9fafb;
  1605. display: flex;
  1606. justify-content: space-between;
  1607. align-items: center;
  1608. gap: 8px;
  1609. }}
  1610. .nav-item.active .nav-item-header {{
  1611. background: rgba(102, 126, 234, 0.1);
  1612. }}
  1613. .nav-item-name {{
  1614. flex: 1;
  1615. font-weight: 600;
  1616. font-size: 14px;
  1617. color: #1f2937;
  1618. overflow: hidden;
  1619. text-overflow: ellipsis;
  1620. white-space: nowrap;
  1621. display: flex;
  1622. align-items: center;
  1623. gap: 6px;
  1624. }}
  1625. .nav-item.active .nav-item-name {{
  1626. color: #667eea;
  1627. }}
  1628. .nav-label {{
  1629. display: inline-block;
  1630. font-size: 11px;
  1631. color: #8b5cf6;
  1632. background: #f5f3ff;
  1633. padding: 2px 6px;
  1634. border-radius: 3px;
  1635. font-weight: 500;
  1636. white-space: nowrap;
  1637. flex-shrink: 0;
  1638. }}
  1639. .nav-item-score {{
  1640. font-size: 13px;
  1641. font-weight: 700;
  1642. padding: 4px 10px;
  1643. border-radius: 6px;
  1644. background: #e5e7eb;
  1645. color: #6b7280;
  1646. }}
  1647. .nav-item.active .nav-item-score {{
  1648. background: #667eea;
  1649. color: white;
  1650. }}
  1651. .nav-item-matches {{
  1652. padding: 10px 14px;
  1653. background: white;
  1654. border-top: 1px solid #e5e7eb;
  1655. }}
  1656. .nav-match-item-compact {{
  1657. padding: 6px 0;
  1658. font-size: 12px;
  1659. display: flex;
  1660. align-items: center;
  1661. gap: 6px;
  1662. line-height: 1.4;
  1663. }}
  1664. .nav-match-item-compact:not(:last-child) {{
  1665. border-bottom: 1px dashed #e5e7eb;
  1666. }}
  1667. .nav-match-label {{
  1668. color: #10b981;
  1669. font-weight: 600;
  1670. font-size: 11px;
  1671. flex-shrink: 0;
  1672. }}
  1673. .nav-match-name-compact {{
  1674. color: #1f2937;
  1675. font-weight: 500;
  1676. flex: 1;
  1677. overflow: hidden;
  1678. text-overflow: ellipsis;
  1679. white-space: nowrap;
  1680. }}
  1681. .nav-match-score-compact {{
  1682. color: #667eea;
  1683. font-weight: 700;
  1684. font-size: 12px;
  1685. flex-shrink: 0;
  1686. }}
  1687. .inspirations-section {{
  1688. flex: 1;
  1689. height: 100vh;
  1690. overflow-y: auto;
  1691. background: #f5f7fa;
  1692. position: relative;
  1693. }}
  1694. .breadcrumb-container {{
  1695. position: sticky;
  1696. top: 0;
  1697. left: 0;
  1698. right: 0;
  1699. background: white;
  1700. border-bottom: 1px solid #e5e7eb;
  1701. padding: 15px 30px;
  1702. z-index: 100;
  1703. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  1704. }}
  1705. .breadcrumb {{
  1706. display: flex;
  1707. align-items: center;
  1708. gap: 8px;
  1709. font-size: 14px;
  1710. color: #6b7280;
  1711. }}
  1712. .breadcrumb-item {{
  1713. display: flex;
  1714. align-items: center;
  1715. gap: 8px;
  1716. }}
  1717. .breadcrumb-separator {{
  1718. color: #d1d5db;
  1719. }}
  1720. .breadcrumb-current {{
  1721. color: #111827;
  1722. font-weight: 600;
  1723. }}
  1724. .breadcrumb-label {{
  1725. display: inline-block;
  1726. font-size: 12px;
  1727. color: #6366f1;
  1728. background: #eef2ff;
  1729. padding: 2px 8px;
  1730. border-radius: 4px;
  1731. font-weight: 500;
  1732. margin-right: 4px;
  1733. }}
  1734. .inspiration-content-wrapper {{
  1735. padding: 30px 40px;
  1736. }}
  1737. .inspirations-grid {{
  1738. display: none;
  1739. }}
  1740. .inspiration-display {{
  1741. width: 100%;
  1742. }}
  1743. .inspiration-detail {{
  1744. display: none;
  1745. }}
  1746. .inspiration-detail.active {{
  1747. display: block;
  1748. }}
  1749. .detail-header {{
  1750. background: white;
  1751. border-radius: 12px;
  1752. padding: 25px 30px;
  1753. margin-bottom: 15px;
  1754. box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  1755. border: 1px solid #e5e7eb;
  1756. }}
  1757. .detail-title-row {{
  1758. display: flex;
  1759. justify-content: space-between;
  1760. align-items: center;
  1761. margin-bottom: 15px;
  1762. }}
  1763. .detail-title-left, .detail-title-right {{
  1764. display: flex;
  1765. align-items: center;
  1766. gap: 10px;
  1767. }}
  1768. .detail-label {{
  1769. font-size: 12px;
  1770. color: #5b21b6;
  1771. background: rgba(109, 40, 217, 0.15);
  1772. padding: 3px 10px;
  1773. border-radius: 4px;
  1774. font-weight: 500;
  1775. }}
  1776. .detail-category {{
  1777. font-size: 18px;
  1778. font-weight: 600;
  1779. color: #374151;
  1780. }}
  1781. .detail-title {{
  1782. font-size: 24px;
  1783. font-weight: 700;
  1784. color: #111827;
  1785. margin-bottom: 12px;
  1786. }}
  1787. .detail-score-section {{
  1788. display: flex;
  1789. align-items: center;
  1790. gap: 12px;
  1791. }}
  1792. .detail-score-label {{
  1793. font-size: 13px;
  1794. color: #6b7280;
  1795. font-weight: 500;
  1796. }}
  1797. .detail-score-value {{
  1798. font-size: 20px;
  1799. font-weight: 700;
  1800. color: #374151;
  1801. }}
  1802. .card-header {{
  1803. display: flex;
  1804. justify-content: space-between;
  1805. align-items: flex-start;
  1806. margin-bottom: 20px;
  1807. gap: 12px;
  1808. }}
  1809. .inspiration-name {{
  1810. font-size: 28px;
  1811. font-weight: 800;
  1812. color: #1a1a1a;
  1813. line-height: 1.4;
  1814. flex: 1;
  1815. }}
  1816. .grade-badge {{
  1817. background: #10b981;
  1818. color: white;
  1819. padding: 6px 14px;
  1820. border-radius: 20px;
  1821. font-size: 12px;
  1822. font-weight: 700;
  1823. white-space: nowrap;
  1824. }}
  1825. .score-section {{
  1826. display: flex;
  1827. align-items: center;
  1828. gap: 25px;
  1829. margin-bottom: 20px;
  1830. padding: 20px;
  1831. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  1832. border-radius: 12px;
  1833. }}
  1834. .score-item {{
  1835. display: flex;
  1836. flex-direction: column;
  1837. align-items: center;
  1838. gap: 8px;
  1839. flex: 1;
  1840. }}
  1841. .main-score {{
  1842. display: flex;
  1843. flex-direction: column;
  1844. align-items: center;
  1845. gap: 8px;
  1846. }}
  1847. .score-circle {{
  1848. width: 90px;
  1849. height: 90px;
  1850. border-radius: 50%;
  1851. border: 6px solid #10b981;
  1852. display: flex;
  1853. align-items: center;
  1854. justify-content: center;
  1855. background: white;
  1856. }}
  1857. .score-value {{
  1858. font-size: 26px;
  1859. font-weight: 800;
  1860. color: #10b981;
  1861. }}
  1862. .score-label {{
  1863. font-size: 12px;
  1864. color: #6b7280;
  1865. font-weight: 600;
  1866. }}
  1867. .sub-scores {{
  1868. flex: 1;
  1869. display: flex;
  1870. flex-direction: column;
  1871. gap: 12px;
  1872. }}
  1873. .sub-score-item {{
  1874. display: flex;
  1875. justify-content: space-between;
  1876. align-items: center;
  1877. padding: 10px 15px;
  1878. background: white;
  1879. border-radius: 8px;
  1880. }}
  1881. .sub-score-label {{
  1882. font-size: 13px;
  1883. color: #6b7280;
  1884. font-weight: 600;
  1885. }}
  1886. .sub-score-value {{
  1887. font-size: 18px;
  1888. font-weight: 700;
  1889. color: #2563eb;
  1890. }}
  1891. .metrics-section {{
  1892. display: flex;
  1893. flex-direction: column;
  1894. gap: 10px;
  1895. margin-bottom: 15px;
  1896. }}
  1897. .metric-item {{
  1898. display: flex;
  1899. align-items: center;
  1900. gap: 8px;
  1901. font-size: 13px;
  1902. color: #4b5563;
  1903. }}
  1904. .metric-icon {{
  1905. font-size: 16px;
  1906. }}
  1907. .metric-label {{
  1908. font-weight: 600;
  1909. }}
  1910. .metric-value {{
  1911. color: #1f2937;
  1912. }}
  1913. .match-preview {{
  1914. background: #f9fafb;
  1915. padding: 12px;
  1916. border-radius: 8px;
  1917. margin-bottom: 10px;
  1918. border-left: 3px solid #8b5cf6;
  1919. }}
  1920. .match-preview-header {{
  1921. font-size: 12px;
  1922. font-weight: 600;
  1923. color: #6b7280;
  1924. margin-bottom: 6px;
  1925. }}
  1926. .match-preview-content {{
  1927. display: flex;
  1928. justify-content: space-between;
  1929. align-items: center;
  1930. }}
  1931. .match-preview-name {{
  1932. font-size: 13px;
  1933. color: #1f2937;
  1934. flex: 1;
  1935. }}
  1936. .match-preview-score {{
  1937. font-size: 16px;
  1938. font-weight: 700;
  1939. }}
  1940. .preview-parts {{
  1941. margin-top: 8px;
  1942. padding: 8px 10px;
  1943. border-radius: 6px;
  1944. font-size: 12px;
  1945. line-height: 1.6;
  1946. }}
  1947. .preview-parts.same {{
  1948. background: #f0fdf4;
  1949. color: #15803d;
  1950. border-left: 3px solid #10b981;
  1951. }}
  1952. .preview-parts.increment {{
  1953. background: #fff7ed;
  1954. color: #92400e;
  1955. border-left: 3px solid #f59e0b;
  1956. margin-top: 6px;
  1957. }}
  1958. .preview-parts strong {{
  1959. font-weight: 700;
  1960. margin-right: 6px;
  1961. }}
  1962. .score-divider {{
  1963. width: 1px;
  1964. height: 40px;
  1965. background: #e5e7eb;
  1966. }}
  1967. .inspiration-category-section {{
  1968. display: grid;
  1969. grid-template-columns: 1fr 1fr;
  1970. gap: 25px;
  1971. margin-bottom: 0;
  1972. }}
  1973. .info-detail-column {{
  1974. background: #fafbfc;
  1975. border-radius: 10px;
  1976. border: 1px solid #e5e7eb;
  1977. overflow: hidden;
  1978. }}
  1979. .info-header {{
  1980. padding: 15px 20px;
  1981. border-bottom: 1px solid #e5e7eb;
  1982. background: #f9fafb;
  1983. display: flex;
  1984. align-items: center;
  1985. gap: 10px;
  1986. }}
  1987. .info-label {{
  1988. font-size: 11px;
  1989. color: #8b5cf6;
  1990. background: #f5f3ff;
  1991. padding: 2px 6px;
  1992. border-radius: 3px;
  1993. font-weight: 500;
  1994. }}
  1995. .info-name {{
  1996. font-size: 16px;
  1997. font-weight: 600;
  1998. color: #111827;
  1999. }}
  2000. .info-content {{
  2001. padding: 20px;
  2002. }}
  2003. .info-field {{
  2004. margin-bottom: 12px;
  2005. line-height: 1.6;
  2006. }}
  2007. .info-field-label {{
  2008. font-weight: 600;
  2009. color: #374151;
  2010. margin-right: 6px;
  2011. }}
  2012. .info-field-value {{
  2013. color: #6b7280;
  2014. }}
  2015. .info-posts {{
  2016. margin-top: 20px;
  2017. }}
  2018. .info-posts-title {{
  2019. font-size: 14px;
  2020. font-weight: 600;
  2021. color: #374151;
  2022. margin-bottom: 12px;
  2023. }}
  2024. .info-posts-grid {{
  2025. display: flex;
  2026. gap: 15px;
  2027. overflow-x: auto;
  2028. overflow-y: hidden;
  2029. padding-bottom: 10px;
  2030. scroll-behavior: smooth;
  2031. }}
  2032. .info-posts-grid::-webkit-scrollbar {{
  2033. height: 6px;
  2034. }}
  2035. .info-posts-grid::-webkit-scrollbar-track {{
  2036. background: #f3f4f6;
  2037. border-radius: 3px;
  2038. }}
  2039. .info-posts-grid::-webkit-scrollbar-thumb {{
  2040. background: #d1d5db;
  2041. border-radius: 3px;
  2042. }}
  2043. .info-posts-grid::-webkit-scrollbar-thumb:hover {{
  2044. background: #9ca3af;
  2045. }}
  2046. .info-posts-grid .search-note-item {{
  2047. flex: 0 0 260px;
  2048. min-width: 260px;
  2049. }}
  2050. .matches-list {{
  2051. display: flex;
  2052. flex-direction: column;
  2053. gap: 15px;
  2054. }}
  2055. .match-item {{
  2056. background: white;
  2057. border-radius: 12px;
  2058. overflow: hidden;
  2059. box-shadow: 0 2px 8px rgba(0,0,0,0.06);
  2060. transition: all 0.3s;
  2061. margin-bottom: 20px;
  2062. }}
  2063. .match-item:hover {{
  2064. box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  2065. }}
  2066. .match-main-header {{
  2067. background: linear-gradient(135deg, #a5b4fc 0%, #c4b5fd 100%);
  2068. border-bottom: none;
  2069. padding: 16px 20px;
  2070. cursor: pointer;
  2071. display: flex;
  2072. justify-content: space-between;
  2073. align-items: center;
  2074. }}
  2075. .match-main-header:hover {{
  2076. background: linear-gradient(135deg, #8b9cfc 0%, #b39dfc 100%);
  2077. }}
  2078. .match-header-row {{
  2079. flex: 1;
  2080. display: flex;
  2081. justify-content: space-between;
  2082. align-items: center;
  2083. gap: 20px;
  2084. }}
  2085. .match-header-left, .match-header-right {{
  2086. display: flex;
  2087. align-items: center;
  2088. gap: 10px;
  2089. flex: 1;
  2090. }}
  2091. .match-header-center {{
  2092. display: flex;
  2093. flex-direction: column;
  2094. align-items: center;
  2095. gap: 4px;
  2096. padding: 0 20px;
  2097. }}
  2098. .match-score-label {{
  2099. font-size: 11px;
  2100. color: #4c1d95;
  2101. text-transform: uppercase;
  2102. }}
  2103. .match-score-value {{
  2104. font-size: 20px;
  2105. font-weight: 700;
  2106. color: #5b21b6;
  2107. }}
  2108. .match-title {{
  2109. color: #3730a3;
  2110. font-size: 16px;
  2111. font-weight: 700;
  2112. }}
  2113. .match-category {{
  2114. color: #3730a3;
  2115. font-size: 16px;
  2116. font-weight: 700;
  2117. }}
  2118. .match-hierarchy {{
  2119. color: #4c1d95;
  2120. font-size: 13px;
  2121. }}
  2122. .match-toggle-main {{
  2123. color: #5b21b6;
  2124. font-size: 20px;
  2125. transition: transform 0.3s;
  2126. flex-shrink: 0;
  2127. }}
  2128. .match-item.expanded .match-toggle-main {{
  2129. transform: rotate(180deg);
  2130. }}
  2131. .match-main-content {{
  2132. max-height: 0;
  2133. overflow: hidden;
  2134. transition: max-height 0.3s ease;
  2135. }}
  2136. .match-item.expanded .match-main-content {{
  2137. max-height: 10000px;
  2138. }}
  2139. .match-main-content .inspiration-category-section {{
  2140. margin: 25px;
  2141. margin-bottom: 0;
  2142. }}
  2143. .step-section-wrapper {{
  2144. margin: 20px 25px;
  2145. border: 2px solid #e5e7eb;
  2146. border-radius: 12px;
  2147. overflow: hidden;
  2148. background: white;
  2149. }}
  2150. .step-1-wrapper {{
  2151. border-color: #fbbf24;
  2152. }}
  2153. .step-2-wrapper {{
  2154. border-color: #34d399;
  2155. }}
  2156. .step-header {{
  2157. background: linear-gradient(135deg, #fbbf24 0%, #fcd34d 100%);
  2158. padding: 16px 25px;
  2159. display: flex;
  2160. justify-content: space-between;
  2161. align-items: center;
  2162. cursor: pointer;
  2163. transition: all 0.3s;
  2164. }}
  2165. .step-header:hover {{
  2166. background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
  2167. }}
  2168. .step-2-wrapper .step-header {{
  2169. background: linear-gradient(135deg, #34d399 0%, #6ee7b7 100%);
  2170. }}
  2171. .step-2-wrapper .step-header:hover {{
  2172. background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
  2173. }}
  2174. .step-header-content {{
  2175. display: flex;
  2176. align-items: center;
  2177. gap: 12px;
  2178. }}
  2179. .step-header .step-toggle {{
  2180. font-size: 20px;
  2181. color: #78350f;
  2182. transition: transform 0.3s;
  2183. }}
  2184. .step-2-wrapper .step-header .step-toggle {{
  2185. color: #065f46;
  2186. }}
  2187. .step-section-wrapper.expanded > .step-header .step-toggle {{
  2188. transform: rotate(180deg);
  2189. }}
  2190. .step-wrapper-content {{
  2191. max-height: 0;
  2192. overflow: hidden;
  2193. transition: max-height 0.3s ease;
  2194. }}
  2195. .step-section-wrapper.expanded .step-wrapper-content {{
  2196. max-height: 30000px;
  2197. }}
  2198. .step-number-badge {{
  2199. background: rgba(255, 255, 255, 0.5);
  2200. color: #78350f;
  2201. padding: 6px 14px;
  2202. border-radius: 20px;
  2203. font-size: 13px;
  2204. font-weight: 700;
  2205. letter-spacing: 0.5px;
  2206. }}
  2207. .step-2-wrapper .step-number-badge {{
  2208. color: #065f46;
  2209. }}
  2210. .step-title {{
  2211. color: #78350f;
  2212. font-size: 16px;
  2213. font-weight: 700;
  2214. }}
  2215. .step-2-wrapper .step-title {{
  2216. color: #065f46;
  2217. }}
  2218. .match-analysis-section {{
  2219. padding: 25px;
  2220. }}
  2221. .match-header {{
  2222. padding: 20px;
  2223. cursor: pointer;
  2224. display: flex;
  2225. justify-content: space-between;
  2226. align-items: center;
  2227. gap: 15px;
  2228. background: #fafafa;
  2229. transition: background 0.3s;
  2230. border-bottom: 1px solid #e5e7eb;
  2231. }}
  2232. .match-header:hover {{
  2233. background: #f5f5f5;
  2234. }}
  2235. .match-section.expanded .match-header {{
  2236. background: rgba(102, 126, 234, 0.05);
  2237. }}
  2238. .match-header-left {{
  2239. flex: 1;
  2240. min-width: 0;
  2241. }}
  2242. .match-rank {{
  2243. display: inline-block;
  2244. padding: 3px 10px;
  2245. border-radius: 4px;
  2246. font-size: 11px;
  2247. font-weight: 600;
  2248. background: rgba(91, 33, 182, 0.15);
  2249. color: #5b21b6;
  2250. }}
  2251. .match-element-name {{
  2252. font-size: 18px;
  2253. font-weight: 700;
  2254. color: #1f2937;
  2255. display: block;
  2256. margin-bottom: 6px;
  2257. }}
  2258. .match-context {{
  2259. font-size: 13px;
  2260. color: #6b7280;
  2261. display: flex;
  2262. align-items: center;
  2263. gap: 6px;
  2264. }}
  2265. .match-context-separator {{
  2266. color: #d1d5db;
  2267. }}
  2268. .match-header-right {{
  2269. display: flex;
  2270. align-items: center;
  2271. gap: 15px;
  2272. flex-shrink: 0;
  2273. }}
  2274. .match-score {{
  2275. font-size: 24px;
  2276. font-weight: 800;
  2277. color: #667eea;
  2278. }}
  2279. .match-toggle {{
  2280. width: 32px;
  2281. height: 32px;
  2282. border-radius: 8px;
  2283. background: #e5e7eb;
  2284. display: flex;
  2285. align-items: center;
  2286. justify-content: center;
  2287. transition: all 0.3s;
  2288. font-size: 18px;
  2289. color: #6b7280;
  2290. }}
  2291. .match-item.expanded .match-toggle {{
  2292. background: #667eea;
  2293. color: white;
  2294. transform: rotate(180deg);
  2295. }}
  2296. .match-section {{
  2297. border-bottom: 1px solid #e5e7eb;
  2298. }}
  2299. .match-section:last-child {{
  2300. border-bottom: none;
  2301. }}
  2302. .step-section-header {{
  2303. padding: 18px 25px;
  2304. background: #f9fafb;
  2305. cursor: pointer;
  2306. display: flex;
  2307. justify-content: space-between;
  2308. align-items: center;
  2309. border-bottom: 1px solid #e5e7eb;
  2310. }}
  2311. .step-section-header:hover {{
  2312. background: #f3f4f6;
  2313. }}
  2314. .step-section-title {{
  2315. font-size: 15px;
  2316. font-weight: 600;
  2317. color: #374151;
  2318. display: flex;
  2319. align-items: center;
  2320. gap: 10px;
  2321. }}
  2322. .step-sub-number {{
  2323. display: inline-flex;
  2324. align-items: center;
  2325. justify-content: center;
  2326. min-width: 38px;
  2327. padding: 4px 10px;
  2328. background: #10b981;
  2329. color: white;
  2330. border-radius: 6px;
  2331. font-size: 12px;
  2332. font-weight: 700;
  2333. }}
  2334. .step-number {{
  2335. display: inline-flex;
  2336. align-items: center;
  2337. justify-content: center;
  2338. width: 20px;
  2339. height: 20px;
  2340. background: #e5e7eb;
  2341. color: #6b7280;
  2342. border-radius: 50%;
  2343. font-size: 11px;
  2344. font-weight: 600;
  2345. }}
  2346. .step-toggle {{
  2347. font-size: 18px;
  2348. color: #6b7280;
  2349. transition: transform 0.3s;
  2350. }}
  2351. .step-section.expanded .step-toggle {{
  2352. transform: rotate(180deg);
  2353. }}
  2354. .step-section-content {{
  2355. max-height: 0;
  2356. overflow: hidden;
  2357. transition: max-height 0.3s ease;
  2358. }}
  2359. .step-section.expanded .step-section-content {{
  2360. max-height: 20000px;
  2361. }}
  2362. .match-content {{
  2363. max-height: 0;
  2364. overflow: hidden;
  2365. transition: max-height 0.3s ease;
  2366. }}
  2367. .match-section.expanded .match-content {{
  2368. max-height: 2000px;
  2369. }}
  2370. .match-content-inner {{
  2371. padding: 20px;
  2372. }}
  2373. .match-explain {{
  2374. background: #fffbeb;
  2375. padding: 16px 20px;
  2376. border-radius: 8px;
  2377. margin-top: 20px;
  2378. border-left: 3px solid #f59e0b;
  2379. }}
  2380. .match-explain-title {{
  2381. font-weight: 600;
  2382. color: #374151;
  2383. margin-bottom: 6px;
  2384. font-size: 13px;
  2385. }}
  2386. .match-explain-text {{
  2387. color: #6b7280;
  2388. font-size: 14px;
  2389. line-height: 1.7;
  2390. }}
  2391. .match-parts-container {{
  2392. display: grid;
  2393. grid-template-columns: 1fr 1fr;
  2394. gap: 25px;
  2395. margin-bottom: 20px;
  2396. }}
  2397. .match-parts-column {{
  2398. min-width: 0;
  2399. }}
  2400. .match-parts {{
  2401. margin-bottom: 0;
  2402. height: 100%;
  2403. background: #f9fafb;
  2404. padding: 16px;
  2405. border-radius: 8px;
  2406. border: 1px solid #e5e7eb;
  2407. }}
  2408. .match-parts-title {{
  2409. font-weight: 700;
  2410. font-size: 14px;
  2411. margin-bottom: 16px;
  2412. color: #374151;
  2413. text-transform: uppercase;
  2414. letter-spacing: 0.5px;
  2415. }}
  2416. .part-item {{
  2417. padding: 10px 0;
  2418. margin-bottom: 8px;
  2419. font-size: 14px;
  2420. border-bottom: 1px solid #e5e7eb;
  2421. }}
  2422. .part-item:last-child {{
  2423. border-bottom: none;
  2424. }}
  2425. .part-key {{
  2426. font-weight: 500;
  2427. color: #374151;
  2428. margin-right: 8px;
  2429. }}
  2430. .part-value {{
  2431. color: #6b7280;
  2432. }}
  2433. .same-parts {{
  2434. background: #ecfdf5;
  2435. border-color: #10b981;
  2436. }}
  2437. .increment-parts {{
  2438. background: #fef3c7;
  2439. border-color: #f59e0b;
  2440. }}
  2441. .search-results-section {{
  2442. padding: 25px;
  2443. }}
  2444. .search-params-section {{
  2445. background: #f0f9ff;
  2446. border: 1px solid #bae6fd;
  2447. border-radius: 8px;
  2448. padding: 20px;
  2449. margin-bottom: 20px;
  2450. }}
  2451. .search-params-title {{
  2452. font-size: 15px;
  2453. font-weight: 700;
  2454. color: #0c4a6e;
  2455. margin-bottom: 16px;
  2456. }}
  2457. .search-params-grid {{
  2458. display: grid;
  2459. grid-template-columns: repeat(2, 1fr);
  2460. gap: 12px;
  2461. }}
  2462. .search-param-item {{
  2463. display: flex;
  2464. align-items: center;
  2465. gap: 8px;
  2466. }}
  2467. .search-param-label {{
  2468. font-size: 13px;
  2469. color: #0369a1;
  2470. font-weight: 600;
  2471. min-width: 80px;
  2472. }}
  2473. .search-param-value {{
  2474. font-size: 13px;
  2475. color: #0c4a6e;
  2476. background: #e0f2fe;
  2477. padding: 4px 12px;
  2478. border-radius: 4px;
  2479. }}
  2480. .search-summary-section {{
  2481. background: #ecfdf5;
  2482. border: 1px solid #a7f3d0;
  2483. border-radius: 8px;
  2484. padding: 16px 20px;
  2485. margin-bottom: 20px;
  2486. }}
  2487. .search-summary-header {{
  2488. display: flex;
  2489. justify-content: space-between;
  2490. align-items: center;
  2491. gap: 20px;
  2492. }}
  2493. .search-summary-left {{
  2494. flex: 1;
  2495. }}
  2496. .search-summary-title {{
  2497. font-size: 15px;
  2498. font-weight: 700;
  2499. color: #065f46;
  2500. margin-bottom: 8px;
  2501. }}
  2502. .search-summary-content {{
  2503. font-size: 14px;
  2504. color: #047857;
  2505. }}
  2506. .search-result-count {{
  2507. font-size: 18px;
  2508. font-weight: 700;
  2509. color: #059669;
  2510. margin: 0 4px;
  2511. }}
  2512. .search-sort-buttons {{
  2513. display: flex;
  2514. align-items: center;
  2515. gap: 8px;
  2516. }}
  2517. .search-sort-label {{
  2518. font-size: 13px;
  2519. color: #047857;
  2520. font-weight: 600;
  2521. }}
  2522. .search-sort-btn {{
  2523. background: white;
  2524. border: 1px solid #a7f3d0;
  2525. color: #065f46;
  2526. padding: 6px 12px;
  2527. border-radius: 6px;
  2528. font-size: 12px;
  2529. font-weight: 600;
  2530. cursor: pointer;
  2531. transition: all 0.2s;
  2532. }}
  2533. .search-sort-btn:hover {{
  2534. background: #d1fae5;
  2535. border-color: #6ee7b7;
  2536. }}
  2537. .search-sort-btn.active {{
  2538. background: #059669;
  2539. color: white;
  2540. border-color: #059669;
  2541. }}
  2542. .note-position {{
  2543. font-size: 11px;
  2544. color: #9ca3af;
  2545. font-weight: 600;
  2546. }}
  2547. .search-notes-list {{
  2548. display: grid;
  2549. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  2550. gap: 16px;
  2551. }}
  2552. .search-note-item {{
  2553. background: white;
  2554. border-radius: 12px;
  2555. overflow: hidden;
  2556. box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  2557. border: 1px solid #e5e7eb;
  2558. transition: all 0.3s;
  2559. position: relative;
  2560. }}
  2561. .search-note-item:hover {{
  2562. box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2);
  2563. border-color: #667eea;
  2564. }}
  2565. .note-score-badge {{
  2566. position: absolute;
  2567. top: 12px;
  2568. right: 12px;
  2569. background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  2570. padding: 8px 14px;
  2571. border-radius: 8px;
  2572. display: flex;
  2573. flex-direction: column;
  2574. align-items: center;
  2575. gap: 2px;
  2576. cursor: pointer;
  2577. z-index: 5;
  2578. box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
  2579. transition: all 0.3s;
  2580. }}
  2581. .note-score-badge:hover {{
  2582. transform: scale(1.05);
  2583. box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
  2584. }}
  2585. .note-score-badge .score-label {{
  2586. font-size: 10px;
  2587. color: rgba(255, 255, 255, 0.9);
  2588. text-transform: uppercase;
  2589. letter-spacing: 0.5px;
  2590. }}
  2591. .note-score-badge .score-value {{
  2592. font-size: 18px;
  2593. font-weight: 700;
  2594. color: white;
  2595. }}
  2596. .note-image-carousel {{
  2597. position: relative;
  2598. width: 100%;
  2599. min-height: 200px;
  2600. max-height: 400px;
  2601. background: #f3f4f6;
  2602. overflow: hidden;
  2603. display: flex;
  2604. align-items: center;
  2605. justify-content: center;
  2606. }}
  2607. .note-images-track {{
  2608. display: flex;
  2609. width: 100%;
  2610. height: 100%;
  2611. transition: transform 0.4s ease;
  2612. }}
  2613. .note-image {{
  2614. flex: 0 0 100%;
  2615. width: 100%;
  2616. height: auto;
  2617. max-height: 400px;
  2618. object-fit: contain;
  2619. }}
  2620. .note-carousel-button {{
  2621. position: absolute;
  2622. top: 50%;
  2623. transform: translateY(-50%);
  2624. width: 32px;
  2625. height: 32px;
  2626. background: rgba(255, 255, 255, 0.9);
  2627. border: none;
  2628. border-radius: 50%;
  2629. display: flex;
  2630. align-items: center;
  2631. justify-content: center;
  2632. cursor: pointer;
  2633. transition: all 0.3s;
  2634. z-index: 10;
  2635. font-size: 16px;
  2636. color: #6b7280;
  2637. opacity: 0;
  2638. }}
  2639. .search-note-item:hover .note-carousel-button {{
  2640. opacity: 1;
  2641. }}
  2642. .note-carousel-button:hover {{
  2643. background: #667eea;
  2644. color: white;
  2645. }}
  2646. .note-carousel-button.disabled {{
  2647. opacity: 0 !important;
  2648. }}
  2649. .note-carousel-button.prev {{
  2650. left: 10px;
  2651. }}
  2652. .note-carousel-button.next {{
  2653. right: 10px;
  2654. }}
  2655. .note-image-dots {{
  2656. position: absolute;
  2657. bottom: 10px;
  2658. left: 50%;
  2659. transform: translateX(-50%);
  2660. display: flex;
  2661. gap: 6px;
  2662. z-index: 10;
  2663. }}
  2664. .note-image-dot {{
  2665. width: 6px;
  2666. height: 6px;
  2667. border-radius: 50%;
  2668. background: rgba(255, 255, 255, 0.6);
  2669. transition: all 0.3s;
  2670. }}
  2671. .note-image-dot.active {{
  2672. background: white;
  2673. width: 20px;
  2674. border-radius: 3px;
  2675. }}
  2676. .note-content {{
  2677. padding: 15px;
  2678. }}
  2679. .note-title {{
  2680. font-size: 15px;
  2681. font-weight: 600;
  2682. color: #1f2937;
  2683. margin-bottom: 8px;
  2684. overflow: hidden;
  2685. text-overflow: ellipsis;
  2686. display: -webkit-box;
  2687. -webkit-line-clamp: 2;
  2688. -webkit-box-orient: vertical;
  2689. line-height: 1.4;
  2690. }}
  2691. .note-desc {{
  2692. font-size: 13px;
  2693. color: #6b7280;
  2694. line-height: 1.6;
  2695. margin-bottom: 12px;
  2696. overflow: hidden;
  2697. text-overflow: ellipsis;
  2698. display: -webkit-box;
  2699. -webkit-line-clamp: 2;
  2700. -webkit-box-orient: vertical;
  2701. }}
  2702. .note-publish-date {{
  2703. font-size: 12px;
  2704. color: #9ca3af;
  2705. margin-bottom: 12px;
  2706. display: flex;
  2707. align-items: center;
  2708. gap: 4px;
  2709. }}
  2710. .note-footer {{
  2711. display: flex;
  2712. justify-content: space-between;
  2713. align-items: center;
  2714. padding-top: 12px;
  2715. border-top: 1px solid #f3f4f6;
  2716. }}
  2717. .note-author {{
  2718. font-size: 12px;
  2719. font-weight: 600;
  2720. color: #6b7280;
  2721. overflow: hidden;
  2722. text-overflow: ellipsis;
  2723. white-space: nowrap;
  2724. max-width: 150px;
  2725. }}
  2726. .note-points-hover {{
  2727. position: absolute;
  2728. bottom: 100%;
  2729. left: 0;
  2730. right: 0;
  2731. background: white;
  2732. border: 1px solid #e5e7eb;
  2733. border-radius: 8px;
  2734. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2735. padding: 12px;
  2736. margin-bottom: 8px;
  2737. opacity: 0;
  2738. visibility: hidden;
  2739. transition: opacity 0.2s, visibility 0.2s;
  2740. z-index: 10;
  2741. max-height: 300px;
  2742. overflow-y: auto;
  2743. }}
  2744. .search-note-item {{
  2745. position: relative;
  2746. }}
  2747. .search-note-item:hover .note-points-hover {{
  2748. opacity: 1;
  2749. visibility: visible;
  2750. }}
  2751. .note-points {{
  2752. /* 用于详情弹窗中的样式 */
  2753. }}
  2754. .points-section {{
  2755. margin-bottom: 8px;
  2756. }}
  2757. .points-section:last-child {{
  2758. margin-bottom: 0;
  2759. }}
  2760. .points-label {{
  2761. font-size: 11px;
  2762. font-weight: 600;
  2763. color: #6b7280;
  2764. margin-bottom: 4px;
  2765. }}
  2766. .points-list {{
  2767. display: flex;
  2768. flex-wrap: wrap;
  2769. gap: 4px;
  2770. }}
  2771. .point-item {{
  2772. display: inline-block;
  2773. }}
  2774. .point-name {{
  2775. display: inline-block;
  2776. font-size: 11px;
  2777. color: #4b5563;
  2778. background: #f3f4f6;
  2779. padding: 2px 6px;
  2780. border-radius: 3px;
  2781. }}
  2782. .note-stats {{
  2783. display: flex;
  2784. gap: 10px;
  2785. font-size: 12px;
  2786. color: #9ca3af;
  2787. }}
  2788. .note-link {{
  2789. display: block;
  2790. text-align: center;
  2791. padding: 8px;
  2792. margin-top: 10px;
  2793. background: rgba(102, 126, 234, 0.1);
  2794. color: #667eea;
  2795. text-decoration: none;
  2796. border-radius: 6px;
  2797. font-size: 13px;
  2798. font-weight: 600;
  2799. transition: all 0.3s;
  2800. }}
  2801. .note-link:hover {{
  2802. background: #667eea;
  2803. color: white;
  2804. }}
  2805. /* 笔记详情Modal样式 */
  2806. .note-detail-modal {{
  2807. display: none;
  2808. position: fixed;
  2809. top: 0;
  2810. left: 0;
  2811. width: 100%;
  2812. height: 100%;
  2813. background: rgba(0, 0, 0, 0.8);
  2814. z-index: 9999;
  2815. overflow-y: auto;
  2816. padding: 40px 20px;
  2817. }}
  2818. .note-detail-modal.active {{
  2819. display: flex;
  2820. align-items: flex-start;
  2821. justify-content: center;
  2822. }}
  2823. .note-detail-content {{
  2824. background: white;
  2825. border-radius: 16px;
  2826. max-width: 1200px;
  2827. width: 100%;
  2828. position: relative;
  2829. margin: auto;
  2830. }}
  2831. .note-detail-close {{
  2832. position: absolute;
  2833. top: 20px;
  2834. right: 20px;
  2835. width: 40px;
  2836. height: 40px;
  2837. background: white;
  2838. border: none;
  2839. border-radius: 50%;
  2840. font-size: 24px;
  2841. cursor: pointer;
  2842. display: flex;
  2843. align-items: center;
  2844. justify-content: center;
  2845. box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  2846. transition: all 0.3s;
  2847. z-index: 10;
  2848. }}
  2849. .note-detail-close:hover {{
  2850. background: #ef4444;
  2851. color: white;
  2852. transform: rotate(90deg);
  2853. }}
  2854. .note-detail-header {{
  2855. padding: 30px;
  2856. border-bottom: 1px solid #e5e7eb;
  2857. }}
  2858. .note-detail-title {{
  2859. font-size: 24px;
  2860. font-weight: 800;
  2861. color: #1f2937;
  2862. margin-bottom: 15px;
  2863. }}
  2864. .note-detail-meta {{
  2865. display: flex;
  2866. align-items: center;
  2867. gap: 20px;
  2868. font-size: 14px;
  2869. color: #6b7280;
  2870. }}
  2871. .note-detail-author {{
  2872. font-weight: 600;
  2873. color: #1f2937;
  2874. }}
  2875. .note-detail-stats {{
  2876. display: flex;
  2877. gap: 15px;
  2878. }}
  2879. .note-detail-body {{
  2880. padding: 30px;
  2881. }}
  2882. .note-detail-desc {{
  2883. font-size: 15px;
  2884. line-height: 1.8;
  2885. color: #4b5563;
  2886. margin-bottom: 30px;
  2887. }}
  2888. .note-detail-points {{
  2889. margin-bottom: 30px;
  2890. background: #f9fafb;
  2891. border-radius: 8px;
  2892. padding: 20px;
  2893. }}
  2894. .detail-points-section {{
  2895. margin-bottom: 20px;
  2896. }}
  2897. .detail-points-section:last-child {{
  2898. margin-bottom: 0;
  2899. }}
  2900. .detail-points-title {{
  2901. font-size: 16px;
  2902. font-weight: 600;
  2903. color: #111827;
  2904. margin-bottom: 12px;
  2905. }}
  2906. .detail-point-item {{
  2907. background: white;
  2908. padding: 12px 15px;
  2909. border-radius: 6px;
  2910. margin-bottom: 10px;
  2911. border-left: 3px solid #e5e7eb;
  2912. }}
  2913. .detail-point-item:last-child {{
  2914. margin-bottom: 0;
  2915. }}
  2916. .detail-point-name {{
  2917. font-size: 14px;
  2918. font-weight: 600;
  2919. color: #374151;
  2920. margin-bottom: 4px;
  2921. }}
  2922. .detail-point-desc {{
  2923. font-size: 13px;
  2924. line-height: 1.6;
  2925. color: #6b7280;
  2926. }}
  2927. .note-detail-images {{
  2928. display: grid;
  2929. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  2930. gap: 15px;
  2931. }}
  2932. .note-detail-image {{
  2933. width: 100%;
  2934. border-radius: 8px;
  2935. cursor: pointer;
  2936. transition: all 0.3s;
  2937. }}
  2938. .note-detail-image:hover {{
  2939. transform: scale(1.05);
  2940. box-shadow: 0 8px 24px rgba(0,0,0,0.2);
  2941. }}
  2942. .note-detail-footer {{
  2943. padding: 20px 30px;
  2944. border-top: 1px solid #e5e7eb;
  2945. display: flex;
  2946. justify-content: center;
  2947. }}
  2948. .note-detail-link {{
  2949. display: inline-block;
  2950. padding: 12px 32px;
  2951. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2952. color: white;
  2953. text-decoration: none;
  2954. border-radius: 10px;
  2955. font-weight: 600;
  2956. transition: all 0.3s;
  2957. }}
  2958. .note-detail-link:hover {{
  2959. transform: translateY(-2px);
  2960. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  2961. }}
  2962. /* Modal样式 */
  2963. .modal-overlay {{
  2964. display: none;
  2965. position: fixed;
  2966. top: 0;
  2967. left: 0;
  2968. right: 0;
  2969. bottom: 0;
  2970. background: rgba(0, 0, 0, 0.8);
  2971. z-index: 1000;
  2972. align-items: center;
  2973. justify-content: center;
  2974. padding: 20px;
  2975. overflow-y: auto;
  2976. }}
  2977. .modal-overlay.active {{
  2978. display: flex;
  2979. }}
  2980. .modal-content {{
  2981. background: white;
  2982. border-radius: 16px;
  2983. max-width: 1200px;
  2984. width: 100%;
  2985. max-height: 90vh;
  2986. overflow-y: auto;
  2987. position: relative;
  2988. }}
  2989. .modal-close {{
  2990. position: sticky;
  2991. top: 0;
  2992. right: 0;
  2993. background: white;
  2994. border: none;
  2995. font-size: 32px;
  2996. color: #6b7280;
  2997. cursor: pointer;
  2998. padding: 15px 20px;
  2999. z-index: 10;
  3000. text-align: right;
  3001. border-bottom: 1px solid #e5e7eb;
  3002. }}
  3003. .modal-close:hover {{
  3004. color: #1f2937;
  3005. }}
  3006. .modal-body {{
  3007. padding: 30px;
  3008. }}
  3009. /* 分数详情Modal样式 */
  3010. .score-detail-content {{
  3011. max-width: 800px;
  3012. }}
  3013. .score-detail-container {{
  3014. padding: 10px;
  3015. }}
  3016. .score-detail-title {{
  3017. font-size: 24px;
  3018. font-weight: 700;
  3019. color: #1f2937;
  3020. margin-bottom: 20px;
  3021. padding-bottom: 15px;
  3022. border-bottom: 2px solid #e5e7eb;
  3023. }}
  3024. .score-detail-score {{
  3025. background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  3026. padding: 20px;
  3027. border-radius: 12px;
  3028. margin-bottom: 20px;
  3029. display: flex;
  3030. align-items: center;
  3031. justify-content: center;
  3032. gap: 10px;
  3033. }}
  3034. .score-detail-label {{
  3035. font-size: 16px;
  3036. color: rgba(255, 255, 255, 0.9);
  3037. font-weight: 600;
  3038. }}
  3039. .score-detail-value {{
  3040. font-size: 32px;
  3041. font-weight: 700;
  3042. color: white;
  3043. }}
  3044. .score-detail-explain {{
  3045. background: #fffbeb;
  3046. padding: 16px 20px;
  3047. border-radius: 8px;
  3048. margin-bottom: 20px;
  3049. border-left: 3px solid #f59e0b;
  3050. }}
  3051. .score-input-info {{
  3052. background: #f0f9ff;
  3053. padding: 16px 20px;
  3054. border-radius: 8px;
  3055. margin-bottom: 20px;
  3056. border-left: 3px solid #3b82f6;
  3057. }}
  3058. .score-input-title {{
  3059. font-weight: 700;
  3060. font-size: 14px;
  3061. margin-bottom: 12px;
  3062. color: #1e40af;
  3063. text-transform: uppercase;
  3064. letter-spacing: 0.5px;
  3065. }}
  3066. .score-input-item {{
  3067. margin-bottom: 12px;
  3068. padding-bottom: 12px;
  3069. border-bottom: 1px solid #bfdbfe;
  3070. }}
  3071. .score-input-item:last-child {{
  3072. margin-bottom: 0;
  3073. padding-bottom: 0;
  3074. border-bottom: none;
  3075. }}
  3076. .score-input-label {{
  3077. font-weight: 600;
  3078. font-size: 13px;
  3079. color: #1e40af;
  3080. margin-bottom: 4px;
  3081. }}
  3082. .score-input-value {{
  3083. font-size: 14px;
  3084. color: #374151;
  3085. line-height: 1.6;
  3086. white-space: pre-wrap;
  3087. word-break: break-word;
  3088. }}
  3089. .score-parts-container {{
  3090. display: grid;
  3091. grid-template-columns: 1fr 1fr;
  3092. gap: 20px;
  3093. }}
  3094. .score-parts {{
  3095. background: #f9fafb;
  3096. padding: 16px;
  3097. border-radius: 8px;
  3098. border: 1px solid #e5e7eb;
  3099. }}
  3100. .score-parts.same-parts {{
  3101. background: #ecfdf5;
  3102. border-color: #10b981;
  3103. }}
  3104. .score-parts.increment-parts {{
  3105. background: #fef3c7;
  3106. border-color: #f59e0b;
  3107. }}
  3108. .score-parts-title {{
  3109. font-weight: 700;
  3110. font-size: 14px;
  3111. margin-bottom: 16px;
  3112. color: #374151;
  3113. text-transform: uppercase;
  3114. letter-spacing: 0.5px;
  3115. }}
  3116. .score-part-item {{
  3117. padding: 10px 0;
  3118. margin-bottom: 8px;
  3119. font-size: 14px;
  3120. border-bottom: 1px solid #e5e7eb;
  3121. }}
  3122. .score-part-item:last-child {{
  3123. border-bottom: none;
  3124. }}
  3125. .score-part-key {{
  3126. font-weight: 500;
  3127. color: #374151;
  3128. margin-right: 8px;
  3129. }}
  3130. .score-part-value {{
  3131. color: #6b7280;
  3132. }}
  3133. .modal-header {{
  3134. margin-bottom: 25px;
  3135. padding-bottom: 20px;
  3136. border-bottom: 2px solid #e5e7eb;
  3137. }}
  3138. .modal-title {{
  3139. font-size: 28px;
  3140. font-weight: 800;
  3141. color: #1a1a1a;
  3142. }}
  3143. .modal-section {{
  3144. margin-bottom: 30px;
  3145. }}
  3146. .modal-section h3 {{
  3147. font-size: 20px;
  3148. font-weight: 700;
  3149. color: #374151;
  3150. margin-bottom: 15px;
  3151. padding-bottom: 10px;
  3152. border-bottom: 2px solid #f3f4f6;
  3153. }}
  3154. .info-grid {{
  3155. display: grid;
  3156. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  3157. gap: 15px;
  3158. }}
  3159. .info-item {{
  3160. background: #f9fafb;
  3161. padding: 12px 16px;
  3162. border-radius: 8px;
  3163. border-left: 3px solid #8b5cf6;
  3164. }}
  3165. .info-label {{
  3166. font-weight: 600;
  3167. color: #6b7280;
  3168. font-size: 13px;
  3169. margin-right: 8px;
  3170. }}
  3171. .info-value {{
  3172. color: #1f2937;
  3173. font-size: 14px;
  3174. }}
  3175. .metrics-grid {{
  3176. display: grid;
  3177. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  3178. gap: 15px;
  3179. }}
  3180. .metric-box {{
  3181. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3182. padding: 20px;
  3183. border-radius: 12px;
  3184. text-align: center;
  3185. color: white;
  3186. }}
  3187. .metric-box.wide {{
  3188. grid-column: span 2;
  3189. }}
  3190. .metric-box-label {{
  3191. font-size: 13px;
  3192. opacity: 0.9;
  3193. margin-bottom: 8px;
  3194. font-weight: 600;
  3195. }}
  3196. .metric-box-value {{
  3197. font-size: 28px;
  3198. font-weight: 700;
  3199. }}
  3200. .metric-box-value.small {{
  3201. font-size: 16px;
  3202. }}
  3203. .step-content {{
  3204. background: #f9fafb;
  3205. padding: 20px;
  3206. border-radius: 12px;
  3207. }}
  3208. .step-field {{
  3209. margin-bottom: 20px;
  3210. }}
  3211. .step-field-label {{
  3212. font-weight: 700;
  3213. color: #374151;
  3214. font-size: 14px;
  3215. margin-bottom: 8px;
  3216. display: block;
  3217. }}
  3218. .step-field-value {{
  3219. color: #1f2937;
  3220. font-size: 15px;
  3221. line-height: 1.7;
  3222. }}
  3223. .matches-list {{
  3224. display: flex;
  3225. flex-direction: column;
  3226. gap: 15px;
  3227. margin-top: 10px;
  3228. }}
  3229. .match-item {{
  3230. background: white;
  3231. padding: 18px;
  3232. border-radius: 10px;
  3233. border-left: 5px solid #3b82f6;
  3234. }}
  3235. .match-item.top1 {{
  3236. border-left-color: #fbbf24;
  3237. background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
  3238. }}
  3239. .match-item.top2 {{
  3240. border-left-color: #c0c0c0;
  3241. background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
  3242. }}
  3243. .match-item.top3 {{
  3244. border-left-color: #cd7f32;
  3245. background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
  3246. }}
  3247. .match-header {{
  3248. display: flex;
  3249. justify-content: space-between;
  3250. align-items: center;
  3251. margin-bottom: 12px;
  3252. gap: 10px;
  3253. }}
  3254. .match-rank {{
  3255. font-size: 18px;
  3256. font-weight: 800;
  3257. color: #6b7280;
  3258. }}
  3259. .match-element-name {{
  3260. flex: 1;
  3261. font-size: 16px;
  3262. font-weight: 700;
  3263. color: #1f2937;
  3264. }}
  3265. .match-score {{
  3266. font-size: 22px;
  3267. font-weight: 800;
  3268. color: #2563eb;
  3269. background: white;
  3270. padding: 6px 14px;
  3271. border-radius: 8px;
  3272. }}
  3273. .match-detail {{
  3274. background: rgba(255, 255, 255, 0.7);
  3275. padding: 10px;
  3276. border-radius: 6px;
  3277. margin-bottom: 10px;
  3278. font-size: 13px;
  3279. color: #4b5563;
  3280. }}
  3281. .match-reason {{
  3282. color: #1f2937;
  3283. font-size: 14px;
  3284. line-height: 1.7;
  3285. }}
  3286. .increment-matches {{
  3287. display: flex;
  3288. flex-direction: column;
  3289. gap: 12px;
  3290. margin-top: 10px;
  3291. }}
  3292. .increment-item {{
  3293. background: white;
  3294. padding: 15px;
  3295. border-radius: 8px;
  3296. border-left: 4px solid #10b981;
  3297. }}
  3298. .increment-header {{
  3299. display: flex;
  3300. justify-content: space-between;
  3301. align-items: center;
  3302. margin-bottom: 10px;
  3303. }}
  3304. .increment-words {{
  3305. font-weight: 700;
  3306. color: #1f2937;
  3307. font-size: 15px;
  3308. }}
  3309. .increment-score {{
  3310. font-size: 20px;
  3311. font-weight: 800;
  3312. color: #10b981;
  3313. }}
  3314. .increment-reason {{
  3315. color: #4b5563;
  3316. font-size: 13px;
  3317. line-height: 1.6;
  3318. }}
  3319. .empty-state {{
  3320. text-align: center;
  3321. padding: 40px;
  3322. color: #9ca3af;
  3323. font-size: 14px;
  3324. }}
  3325. .modal-link {{
  3326. margin-top: 25px;
  3327. padding-top: 20px;
  3328. border-top: 2px solid #e5e7eb;
  3329. text-align: center;
  3330. }}
  3331. .modal-link-btn {{
  3332. display: inline-flex;
  3333. align-items: center;
  3334. gap: 10px;
  3335. padding: 12px 24px;
  3336. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3337. color: white;
  3338. text-decoration: none;
  3339. border-radius: 10px;
  3340. font-size: 15px;
  3341. font-weight: 600;
  3342. transition: all 0.3s;
  3343. }}
  3344. .modal-link-btn:hover {{
  3345. transform: translateY(-2px);
  3346. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  3347. }}
  3348. .timestamp {{
  3349. text-align: center;
  3350. color: white;
  3351. font-size: 13px;
  3352. margin-top: 30px;
  3353. opacity: 0.8;
  3354. }}
  3355. .match-context {{
  3356. background: #f3f4f6;
  3357. padding: 8px 12px;
  3358. border-radius: 6px;
  3359. margin: 8px 0;
  3360. font-size: 12px;
  3361. color: #6b7280;
  3362. line-height: 1.6;
  3363. }}
  3364. .match-explain {{
  3365. background: #fef3c7;
  3366. padding: 10px 12px;
  3367. border-radius: 6px;
  3368. margin: 10px 0;
  3369. font-size: 13px;
  3370. color: #92400e;
  3371. line-height: 1.7;
  3372. border-left: 3px solid #f59e0b;
  3373. }}
  3374. .match-parts {{
  3375. margin: 12px 0;
  3376. border-radius: 8px;
  3377. overflow: hidden;
  3378. }}
  3379. .match-parts.same-parts {{
  3380. background: #f0fdf4;
  3381. border: 2px solid #10b981;
  3382. }}
  3383. .match-parts.increment-parts {{
  3384. background: #fff7ed;
  3385. border: 2px solid #f59e0b;
  3386. }}
  3387. .parts-header {{
  3388. font-weight: 700;
  3389. padding: 10px 12px;
  3390. font-size: 13px;
  3391. }}
  3392. .same-parts .parts-header {{
  3393. background: #dcfce7;
  3394. color: #15803d;
  3395. }}
  3396. .increment-parts .parts-header {{
  3397. background: #fed7aa;
  3398. color: #92400e;
  3399. }}
  3400. .parts-content {{
  3401. padding: 8px 12px;
  3402. }}
  3403. .part-item {{
  3404. padding: 6px 0;
  3405. border-bottom: 1px solid rgba(0,0,0,0.05);
  3406. font-size: 13px;
  3407. line-height: 1.6;
  3408. }}
  3409. .part-item:last-child {{
  3410. border-bottom: none;
  3411. }}
  3412. .part-key {{
  3413. font-weight: 600;
  3414. color: #374151;
  3415. margin-right: 6px;
  3416. }}
  3417. .part-value {{
  3418. color: #1f2937;
  3419. }}
  3420. .increment-context {{
  3421. background: #fef3c7;
  3422. padding: 10px 12px;
  3423. border-radius: 6px;
  3424. margin: 10px 0;
  3425. font-size: 12px;
  3426. color: #92400e;
  3427. line-height: 1.6;
  3428. border-left: 3px solid #f59e0b;
  3429. }}
  3430. @media (max-width: 768px) {{
  3431. .main-content-layout {{
  3432. flex-direction: column;
  3433. }}
  3434. .sidebar-nav {{
  3435. position: relative;
  3436. width: 100%;
  3437. max-height: 300px;
  3438. }}
  3439. .inspirations-grid {{
  3440. grid-template-columns: 1fr;
  3441. }}
  3442. .header h1 {{
  3443. font-size: 32px;
  3444. }}
  3445. .stats-overview {{
  3446. grid-template-columns: repeat(2, 1fr);
  3447. }}
  3448. }}
  3449. </style>
  3450. </head>
  3451. <body>
  3452. <div class="container">
  3453. <div class="main-content-layout">
  3454. <div class="sidebar-nav">
  3455. <div class="nav-title">📑 灵感点列表</div>
  3456. <div class="nav-list" id="navList">
  3457. <!-- 目录将通过JS动态生成 -->
  3458. </div>
  3459. </div>
  3460. <div class="inspirations-section">
  3461. <div class="inspiration-display">
  3462. {cards_html_str}
  3463. </div>
  3464. </div>
  3465. </div>
  3466. <!-- Modal -->
  3467. <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
  3468. <div class="modal-content">
  3469. <button class="modal-close" onclick="closeModal()">&times;</button>
  3470. <div class="modal-body" id="modalBody">
  3471. <!-- Content will be inserted here -->
  3472. </div>
  3473. </div>
  3474. </div>
  3475. <!-- Score Detail Modal -->
  3476. <div id="scoreDetailModal" class="modal-overlay" onclick="closeScoreDetailModal(event)">
  3477. <div class="modal-content score-detail-content">
  3478. <button class="modal-close" onclick="closeScoreDetail()">&times;</button>
  3479. <div class="modal-body" id="scoreModalBody">
  3480. <!-- Score detail content will be inserted here -->
  3481. </div>
  3482. </div>
  3483. </div>
  3484. </div>
  3485. <script>
  3486. {detail_modal_js}
  3487. </script>
  3488. </body>
  3489. </html>'''
  3490. # 写入文件
  3491. output_file = Path(output_path)
  3492. output_file.parent.mkdir(parents=True, exist_ok=True)
  3493. with open(output_file, 'w', encoding='utf-8') as f:
  3494. f.write(html_content)
  3495. return str(output_file.absolute())
  3496. def load_persona_data(persona_path: str) -> Dict[str, Any]:
  3497. """
  3498. 加载人设数据
  3499. Args:
  3500. persona_path: 人设JSON文件路径
  3501. Returns:
  3502. 人设数据字典
  3503. """
  3504. try:
  3505. with open(persona_path, 'r', encoding='utf-8') as f:
  3506. return json.load(f)
  3507. except Exception as e:
  3508. print(f"警告: 读取人设文件失败: {e}")
  3509. return {}
  3510. def main():
  3511. """主函数"""
  3512. import sys
  3513. import os
  3514. # 配置路径(使用当前脚本的相对路径)
  3515. script_dir = os.path.dirname(os.path.abspath(__file__))
  3516. base_dir = os.path.join(script_dir, "data/阿里多多酱")
  3517. inspiration_dir = os.path.join(base_dir, "out/人设_1110/how/灵感点")
  3518. posts_dir = os.path.join(base_dir, "作者历史帖子")
  3519. persona_path = os.path.join(base_dir, "out/人设_1110/人设.json")
  3520. inspiration_to_post_path = os.path.join(base_dir, "out/人设_1110/点到帖子映射.json")
  3521. category_index_path = os.path.join(base_dir, "out/人设_1110/分类索引_完整.json")
  3522. post_to_mapping_path = os.path.join(base_dir, "out/人设_1110/帖子到分类和点映射.json")
  3523. output_path = os.path.join(base_dir, "out/人设_1110/how/灵感点可视化.html")
  3524. print("=" * 60)
  3525. print("灵感点分析可视化脚本")
  3526. print("=" * 60)
  3527. # 加载数据
  3528. print("\n📂 正在加载灵感点数据...")
  3529. inspirations_data = load_inspiration_points_data(inspiration_dir)
  3530. print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
  3531. print("\n📂 正在加载帖子数据...")
  3532. posts_map = load_posts_data(posts_dir)
  3533. print(f"✅ 成功加载 {len(posts_map)} 个帖子")
  3534. print("\n📂 正在加载人设数据...")
  3535. persona_data = load_persona_data(persona_path)
  3536. print(f"✅ 成功加载人设数据")
  3537. print("\n📂 正在加载点到帖子映射数据...")
  3538. with open(inspiration_to_post_path, 'r', encoding='utf-8') as f:
  3539. inspiration_to_post_data = json.load(f)
  3540. print(f"✅ 成功加载点到帖子映射数据")
  3541. print("\n📂 正在加载分类索引数据...")
  3542. with open(category_index_path, 'r', encoding='utf-8') as f:
  3543. category_index_data = json.load(f)
  3544. print(f"✅ 成功加载分类索引数据")
  3545. print("\n📂 正在加载帖子到分类和点映射数据...")
  3546. with open(post_to_mapping_path, 'r', encoding='utf-8') as f:
  3547. post_to_mapping_data = json.load(f)
  3548. print(f"✅ 成功加载帖子到分类和点映射数据")
  3549. # 生成HTML
  3550. print("\n🎨 正在生成可视化HTML...")
  3551. result_path = generate_html(
  3552. inspirations_data,
  3553. posts_map,
  3554. persona_data,
  3555. output_path,
  3556. inspiration_to_post_data,
  3557. category_index_data,
  3558. post_to_mapping_data
  3559. )
  3560. print(f"\n✅ 可视化文件已生成!")
  3561. print(f"📄 文件路径: {result_path}")
  3562. print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
  3563. print("=" * 60)
  3564. if __name__ == "__main__":
  3565. main()