visualize_inspiration_points.py 93 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970
  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. 加载所有灵感点的分析结果(包含 step1 和 step3)
  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. # 查找 step1 文件
  24. step1_files = list(subdir.glob("all_step1_*.json"))
  25. # 查找 step3 文件
  26. step3_files = list(subdir.glob("all_step3_*.json"))
  27. if step1_files:
  28. try:
  29. # 读取 step1
  30. with open(step1_files[0], 'r', encoding='utf-8') as f:
  31. step1_data = json.load(f)
  32. # 尝试读取 step3
  33. step3_data = None
  34. if step3_files:
  35. try:
  36. with open(step3_files[0], 'r', encoding='utf-8') as f:
  37. step3_data = json.load(f)
  38. except Exception as e:
  39. print(f"警告: 读取 {step3_files[0]} 失败: {e}")
  40. results.append({
  41. "step1": step1_data,
  42. "step3": step3_data,
  43. "inspiration_name": subdir.name
  44. })
  45. except Exception as e:
  46. print(f"警告: 读取 {step1_files[0]} 失败: {e}")
  47. return results
  48. def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]:
  49. """
  50. 加载所有帖子详情数据
  51. Args:
  52. posts_dir: 帖子目录路径
  53. Returns:
  54. 帖子ID到帖子详情的映射
  55. """
  56. posts_path = Path(posts_dir)
  57. posts_map = {}
  58. for post_file in posts_path.glob("*.json"):
  59. try:
  60. with open(post_file, 'r', encoding='utf-8') as f:
  61. post_data = json.load(f)
  62. post_id = post_data.get("channel_content_id")
  63. if post_id:
  64. posts_map[post_id] = post_data
  65. except Exception as e:
  66. print(f"警告: 读取 {post_file} 失败: {e}")
  67. return posts_map
  68. def generate_inspiration_card_html(inspiration_data: Dict[str, Any]) -> str:
  69. """
  70. 生成单个灵感点的卡片HTML
  71. Args:
  72. inspiration_data: 灵感点数据
  73. Returns:
  74. HTML字符串
  75. """
  76. step1 = inspiration_data.get("step1", {})
  77. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  78. # 从step1中获取top1匹配分数
  79. step1_matches = step1.get("匹配结果列表", []) if step1 else []
  80. step1_score = 0
  81. step1_match_element = ""
  82. if step1_matches:
  83. top_match = step1_matches[0]
  84. match_result = top_match.get("匹配结果", {})
  85. step1_score = match_result.get("score", 0)
  86. input_info = top_match.get("输入信息", {})
  87. step1_match_element = input_info.get("A_名称", "")
  88. # 确定卡片颜色(基于Step1分数)
  89. if step1_score >= 0.7:
  90. border_color = "#10b981"
  91. step1_color = "#10b981"
  92. elif step1_score >= 0.5:
  93. border_color = "#f59e0b"
  94. step1_color = "#f59e0b"
  95. elif step1_score >= 0.3:
  96. border_color = "#3b82f6"
  97. step1_color = "#3b82f6"
  98. else:
  99. border_color = "#ef4444"
  100. step1_color = "#ef4444"
  101. # 转义HTML
  102. inspiration_name_escaped = html_module.escape(inspiration_name)
  103. step1_match_element_escaped = html_module.escape(step1_match_element)
  104. # 获取Step1匹配结果(简要展示Top3)
  105. step1_match_preview = ""
  106. if step1_matches:
  107. preview_items = []
  108. for idx, match in enumerate(step1_matches[:3]):
  109. input_info = match.get("输入信息", {})
  110. match_result = match.get("匹配结果", {})
  111. element_name = input_info.get("A_名称", "")
  112. match_score = match_result.get("score", 0)
  113. # 确定排名标记
  114. rank_emoji = ["🥇", "🥈", "🥉"][idx]
  115. preview_items.append(f'''
  116. <div class="preview-match-item">
  117. <span class="preview-rank">{rank_emoji}</span>
  118. <span class="preview-name">{html_module.escape(element_name)}</span>
  119. <span class="preview-score" style="color: {step1_color};">{match_score:.2f}</span>
  120. </div>
  121. ''')
  122. step1_match_preview = f'''
  123. <div class="match-preview">
  124. <div class="match-preview-header">🎯 Top3 匹配要素</div>
  125. <div class="match-preview-list">
  126. {"".join(preview_items)}
  127. </div>
  128. </div>
  129. '''
  130. # 获取 Step3 生成的灵感点(简要展示)
  131. step3_preview = ""
  132. step3 = inspiration_data.get("step3")
  133. if step3:
  134. step3_inspirations = step3.get("灵感点列表", [])
  135. if step3_inspirations:
  136. preview_items = []
  137. for idx, item in enumerate(step3_inspirations[:3]):
  138. path = item.get("推理路径", "")
  139. insp_point = item.get("灵感点", "")
  140. preview_items.append(f'''
  141. <div class="step3-preview-item">
  142. <span class="step3-point">💡 {html_module.escape(insp_point)}</span>
  143. <span class="step3-path">{html_module.escape(path)}</span>
  144. </div>
  145. ''')
  146. step3_preview = f'''
  147. <div class="step3-preview">
  148. <div class="step3-preview-header">✨ Step3 生成的灵感点 (前3个,共{len(step3_inspirations)}个)</div>
  149. <div class="step3-preview-list">
  150. {"".join(preview_items)}
  151. </div>
  152. </div>
  153. '''
  154. # 准备详细数据用于弹窗
  155. detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
  156. detail_data_json_escaped = html_module.escape(detail_data_json)
  157. # 生成详细HTML并进行HTML转义(保留兼容性)
  158. detail_html = generate_detail_html(inspiration_data)
  159. detail_html_escaped = html_module.escape(detail_html)
  160. # 生成 step1 和 step3 独立详情
  161. step1_detail_html = generate_step1_detail_html(inspiration_data)
  162. step1_detail_html_escaped = html_module.escape(step1_detail_html)
  163. step3_detail_html = generate_step3_detail_html(inspiration_data)
  164. step3_detail_html_escaped = html_module.escape(step3_detail_html)
  165. html = f'''
  166. <div class="inspiration-card" style="border-left-color: {border_color};"
  167. data-inspiration-name="{inspiration_name_escaped}"
  168. data-detail="{detail_data_json_escaped}"
  169. data-detail-html="{detail_html_escaped}"
  170. data-step1-detail-html="{step1_detail_html_escaped}"
  171. data-step3-detail-html="{step3_detail_html_escaped}"
  172. data-step1-score="{step1_score}">
  173. <div class="card-header">
  174. <h3 class="inspiration-name">💡 {inspiration_name_escaped}</h3>
  175. </div>
  176. <div class="score-section">
  177. <div class="score-item">
  178. <div class="score-label">Top1 分数</div>
  179. <div class="score-value" style="color: {step1_color};">{step1_score:.3f}</div>
  180. </div>
  181. <div class="score-item">
  182. <div class="score-label">🎯 匹配要素</div>
  183. <div class="score-match-element">{step1_match_element_escaped}</div>
  184. </div>
  185. </div>
  186. {step1_match_preview}
  187. {step3_preview}
  188. <div class="card-actions">
  189. <button class="action-btn btn-step1" onclick="event.stopPropagation(); showStep1Detail(this.closest('.inspiration-card'))">
  190. 🎯 查看匹配详情
  191. </button>
  192. {'<button class="action-btn btn-step3" onclick="event.stopPropagation(); showStep3Detail(this.closest' + "('.inspiration-card')" + ')">✨ 查看生成灵感</button>' if step3 else ''}
  193. </div>
  194. </div>
  195. '''
  196. return html
  197. def generate_step1_detail_html(inspiration_data: Dict[str, Any]) -> str:
  198. """
  199. 生成 step1 匹配详情的HTML
  200. Args:
  201. inspiration_data: 灵感点数据
  202. Returns:
  203. step1 详细信息的HTML字符串
  204. """
  205. import html as html_module
  206. step1 = inspiration_data.get("step1", {})
  207. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  208. content = f'''
  209. <div class="modal-header">
  210. <h2 class="modal-title">🎯 匹配详情: {html_module.escape(inspiration_name)}</h2>
  211. </div>
  212. '''
  213. # 获取元数据
  214. metadata = step1.get("元数据", {})
  215. # Step1 详细信息
  216. if step1 and step1.get("灵感"):
  217. inspiration = step1.get("灵感", "")
  218. matches = step1.get("匹配结果列表", [])
  219. content += f'''
  220. <div class="modal-section">
  221. <h3>🎯 灵感人设匹配分析</h3>
  222. <div class="step-content">
  223. <div class="step-field">
  224. <span class="step-field-label">💡 灵感点内容:</span>
  225. <span class="step-field-value">{html_module.escape(inspiration)}</span>
  226. </div>
  227. '''
  228. # 显示匹配结果(显示Top5)
  229. if matches:
  230. content += f'''
  231. <div class="step-field">
  232. <span class="step-field-label">匹配结果 (Top {min(len(matches), 5)}):</span>
  233. <div class="matches-list">
  234. '''
  235. for index, match in enumerate(matches[:5]):
  236. input_info = match.get("输入信息", {})
  237. match_result = match.get("匹配结果", {})
  238. business_info = match.get("业务信息", {})
  239. element_name = input_info.get("A_名称", "")
  240. element_def = input_info.get("A_定义", "")
  241. context_a = input_info.get("A_Context", "")
  242. score = match_result.get("score", 0)
  243. score_explain = match_result.get("score说明", "")
  244. # 获取匹配关系
  245. match_relations = match_result.get("匹配关系", [])
  246. # 添加排名样式类
  247. rank_class = ""
  248. if index == 0:
  249. rank_class = "top1"
  250. elif index == 1:
  251. rank_class = "top2"
  252. elif index == 2:
  253. rank_class = "top3"
  254. content += f'''
  255. <div class="match-item {rank_class}">
  256. <div class="match-header">
  257. <span class="match-rank">#{index + 1}</span>
  258. <span class="match-element-name">🎯 人设要素: {html_module.escape(element_name)}</span>
  259. <span class="match-score">{score:.2f}</span>
  260. </div>
  261. '''
  262. if element_def:
  263. content += f'<div class="match-context"><strong>📖 定义:</strong> {html_module.escape(element_def)}</div>'
  264. if context_a:
  265. content += f'<div class="match-context"><strong>📍 所属:</strong> {html_module.escape(context_a)}</div>'
  266. # 显示B语义分析、匹配关系、A语义分析(三列布局)
  267. b_semantic = match_result.get("B语义分析", {})
  268. a_semantic = match_result.get("A语义分析", {})
  269. if b_semantic or a_semantic or match_relations:
  270. content += '<div class="semantic-analysis-with-relations">'
  271. # B语义分析
  272. if b_semantic:
  273. content += '''
  274. <div class="semantic-section b-semantic">
  275. <div class="semantic-header">💡 灵感点语义分析 (B)</div>
  276. <div class="semantic-content">
  277. '''
  278. b_substance = b_semantic.get("实质", {})
  279. b_form = b_semantic.get("形式", {})
  280. if b_substance:
  281. content += '<div class="semantic-part"><strong>实质:</strong></div>'
  282. for key, value in b_substance.items():
  283. content += f'''
  284. <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
  285. <span class="semantic-key">{html_module.escape(key)}:</span>
  286. <span class="semantic-value">{html_module.escape(value)}</span>
  287. </div>
  288. '''
  289. if b_form:
  290. content += '<div class="semantic-part"><strong>形式:</strong></div>'
  291. for key, value in b_form.items():
  292. content += f'''
  293. <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
  294. <span class="semantic-key">{html_module.escape(key)}:</span>
  295. <span class="semantic-value">{html_module.escape(value)}</span>
  296. </div>
  297. '''
  298. content += '''
  299. </div>
  300. </div>
  301. '''
  302. # 显示匹配关系(中间)
  303. if match_relations:
  304. content += '''
  305. <div class="match-relations-middle">
  306. <div class="relations-header">🔗 匹配关系</div>
  307. <div class="relations-content">
  308. '''
  309. for idx, rel in enumerate(match_relations):
  310. b_sem = rel.get("B语义", "")
  311. a_sem = rel.get("A语义", "")
  312. relation = rel.get("关系", "")
  313. explanation = rel.get("说明", "")
  314. distance_score = rel.get("距离分数", None)
  315. # 构建中间关系文本
  316. relation_middle = f'{html_module.escape(relation)}'
  317. if distance_score is not None:
  318. relation_middle += f' (分数: {distance_score:.2f})'
  319. # 为每个关系项生成唯一ID,用于连接线定位
  320. rel_id = f'rel_{index}_{idx}'
  321. b_sem_id = f'b_sem_{index}_{idx}'
  322. a_sem_id = f'a_sem_{index}_{idx}'
  323. content += f'''
  324. <div class="relation-item" id="{rel_id}">
  325. <div class="relation-badge">{relation_middle}</div>
  326. <div class="relation-semantics-ref">
  327. <div class="semantic-ref b-ref" data-semantic="{html_module.escape(b_sem)}">B: {html_module.escape(b_sem)}</div>
  328. <div class="relation-arrow">→</div>
  329. <div class="semantic-ref a-ref" data-semantic="{html_module.escape(a_sem)}">A: {html_module.escape(a_sem)}</div>
  330. </div>
  331. <div class="relation-explain">
  332. {html_module.escape(explanation)}
  333. </div>
  334. </div>
  335. '''
  336. content += '''
  337. </div>
  338. </div>
  339. '''
  340. # A语义分析
  341. if a_semantic:
  342. content += '''
  343. <div class="semantic-section a-semantic">
  344. <div class="semantic-header">🎯 人设要素语义分析 (A)</div>
  345. <div class="semantic-content">
  346. '''
  347. a_substance = a_semantic.get("实质", {})
  348. a_form = a_semantic.get("形式", {})
  349. if a_substance:
  350. content += '<div class="semantic-part"><strong>实质:</strong></div>'
  351. for key, value in a_substance.items():
  352. content += f'''
  353. <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
  354. <span class="semantic-key">{html_module.escape(key)}:</span>
  355. <span class="semantic-value">{html_module.escape(value)}</span>
  356. </div>
  357. '''
  358. if a_form:
  359. content += '<div class="semantic-part"><strong>形式:</strong></div>'
  360. for key, value in a_form.items():
  361. content += f'''
  362. <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
  363. <span class="semantic-key">{html_module.escape(key)}:</span>
  364. <span class="semantic-value">{html_module.escape(value)}</span>
  365. </div>
  366. '''
  367. content += '''
  368. </div>
  369. </div>
  370. '''
  371. content += '</div>' # end semantic-analysis-with-relations
  372. # 显示分数详情(放在最后)
  373. rule_score = match_result.get("规则分数", 0)
  374. rule_score_explain = match_result.get("规则分数说明", "")
  375. relevance_score = match_result.get("相关性分数", 0)
  376. relevance_explain = match_result.get("相关性说明", "")
  377. score_explain = match_result.get("score说明", "")
  378. if rule_score or relevance_score or score_explain:
  379. content += '<div class="score-calculation-section">'
  380. if rule_score or relevance_score:
  381. content += '<div class="score-details">'
  382. if rule_score:
  383. content += f'''
  384. <div class="score-detail-item rule-score">
  385. <div class="score-detail-header">
  386. <span class="score-detail-label">📐 规则分数</span>
  387. <span class="score-detail-value">{rule_score:.2f}</span>
  388. </div>
  389. {f'<div class="score-detail-explain">{html_module.escape(rule_score_explain)}</div>' if rule_score_explain else ''}
  390. </div>
  391. '''
  392. if relevance_score:
  393. content += f'''
  394. <div class="score-detail-item relevance-score">
  395. <div class="score-detail-header">
  396. <span class="score-detail-label">🎯 相关性分数</span>
  397. <span class="score-detail-value">{relevance_score:.2f}</span>
  398. </div>
  399. {f'<div class="score-detail-explain">{html_module.escape(relevance_explain)}</div>' if relevance_explain else ''}
  400. </div>
  401. '''
  402. content += '</div>'
  403. if score_explain:
  404. content += f'<div class="match-explain final-score"><strong>🎲 最终分数计算:</strong> {html_module.escape(score_explain)}</div>'
  405. content += '</div>'
  406. content += '''
  407. </div>
  408. '''
  409. content += '''
  410. </div>
  411. </div>
  412. '''
  413. content += '''
  414. </div>
  415. </div>
  416. '''
  417. # 日志链接
  418. if metadata.get("log_url"):
  419. content += f'''
  420. <div class="modal-link">
  421. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  422. 🔗 查看详细日志
  423. </a>
  424. </div>
  425. '''
  426. return content
  427. def generate_step3_detail_html(inspiration_data: Dict[str, Any]) -> str:
  428. """
  429. 生成 step3 生成灵感的详情HTML
  430. Args:
  431. inspiration_data: 灵感点数据
  432. Returns:
  433. step3 详细信息的HTML字符串
  434. """
  435. import html as html_module
  436. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  437. step3 = inspiration_data.get("step3")
  438. content = f'''
  439. <div class="modal-header">
  440. <h2 class="modal-title">✨ 生成灵感: {html_module.escape(inspiration_name)}</h2>
  441. </div>
  442. '''
  443. if not step3:
  444. content += '''
  445. <div class="empty-state">暂无生成的灵感点数据</div>
  446. '''
  447. return content
  448. # 获取元数据
  449. metadata = step3.get("元数据", {})
  450. anchor_info = step3.get("锚点信息", {})
  451. step3_inspirations = step3.get("灵感点列表", [])
  452. anchor_category = anchor_info.get("锚点分类", "")
  453. category_def = anchor_info.get("分类定义", "")
  454. category_context = anchor_info.get("分类上下文", "")
  455. content += f'''
  456. <div class="modal-section">
  457. <h3>🎯 锚点信息</h3>
  458. <div class="step-content">
  459. <div class="step-field">
  460. <span class="step-field-label">🎯 锚点分类:</span>
  461. <span class="step-field-value">{html_module.escape(anchor_category)}</span>
  462. </div>
  463. '''
  464. if category_def:
  465. content += f'''
  466. <div class="step-field">
  467. <span class="step-field-label">📖 分类定义:</span>
  468. <span class="step-field-value">{html_module.escape(category_def)}</span>
  469. </div>
  470. '''
  471. if category_context:
  472. content += f'''
  473. <div class="step-field">
  474. <span class="step-field-label">📍 分类上下文:</span>
  475. <span class="step-field-value">{html_module.escape(category_context)}</span>
  476. </div>
  477. '''
  478. content += '''
  479. </div>
  480. </div>
  481. '''
  482. # 生成的灵感点列表
  483. if step3_inspirations:
  484. content += f'''
  485. <div class="modal-section">
  486. <h3>✨ 生成的灵感点 (共 {len(step3_inspirations)} 个)</h3>
  487. <div class="step-content">
  488. <div class="step3-inspirations-list">
  489. '''
  490. for idx, item in enumerate(step3_inspirations):
  491. path = item.get("推理路径", "")
  492. insp_point = item.get("灵感点", "")
  493. description = item.get("描述", "")
  494. content += f'''
  495. <div class="step3-inspiration-item">
  496. <div class="step3-header">
  497. <span class="step3-rank">#{idx + 1}</span>
  498. <span class="step3-point">{html_module.escape(insp_point)}</span>
  499. </div>
  500. <div class="step3-path"><strong>推理路径:</strong> {html_module.escape(path)}</div>
  501. <div class="step3-desc"><strong>描述:</strong> {html_module.escape(description)}</div>
  502. </div>
  503. '''
  504. content += '''
  505. </div>
  506. </div>
  507. </div>
  508. '''
  509. # 日志链接
  510. if metadata.get("log_url"):
  511. content += f'''
  512. <div class="modal-link">
  513. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  514. 🔗 查看详细日志
  515. </a>
  516. </div>
  517. '''
  518. return content
  519. def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
  520. """
  521. 生成灵感点的详细信息HTML
  522. Args:
  523. inspiration_data: 灵感点数据
  524. Returns:
  525. 详细信息的HTML字符串
  526. """
  527. import html as html_module
  528. step1 = inspiration_data.get("step1", {})
  529. inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
  530. content = f'''
  531. <div class="modal-header">
  532. <h2 class="modal-title">💡 灵感点: {html_module.escape(inspiration_name)}</h2>
  533. </div>
  534. '''
  535. # 获取元数据,用于后面的日志链接
  536. metadata = step1.get("元数据", {})
  537. # Step1 详细信息
  538. if step1 and step1.get("灵感"):
  539. inspiration = step1.get("灵感", "")
  540. matches = step1.get("匹配结果列表", [])
  541. content += f'''
  542. <div class="modal-section">
  543. <h3>🎯 灵感人设匹配分析</h3>
  544. <div class="step-content">
  545. <div class="step-field">
  546. <span class="step-field-label">💡 灵感点内容:</span>
  547. <span class="step-field-value">{html_module.escape(inspiration)}</span>
  548. </div>
  549. '''
  550. # 显示匹配结果(显示Top5)
  551. if matches:
  552. content += f'''
  553. <div class="step-field">
  554. <span class="step-field-label">匹配结果 (Top {min(len(matches), 5)}):</span>
  555. <div class="matches-list">
  556. '''
  557. for index, match in enumerate(matches[:5]):
  558. input_info = match.get("输入信息", {})
  559. match_result = match.get("匹配结果", {})
  560. business_info = match.get("业务信息", {})
  561. element_name = input_info.get("A_名称", "")
  562. element_def = input_info.get("A_定义", "")
  563. context_a = input_info.get("A_Context", "")
  564. score = match_result.get("score", 0)
  565. score_explain = match_result.get("score说明", "")
  566. # 获取匹配关系
  567. match_relations = match_result.get("匹配关系", [])
  568. # 添加排名样式类
  569. rank_class = ""
  570. if index == 0:
  571. rank_class = "top1"
  572. elif index == 1:
  573. rank_class = "top2"
  574. elif index == 2:
  575. rank_class = "top3"
  576. content += f'''
  577. <div class="match-item {rank_class}">
  578. <div class="match-header">
  579. <span class="match-rank">#{index + 1}</span>
  580. <span class="match-element-name">🎯 人设要素: {html_module.escape(element_name)}</span>
  581. <span class="match-score">{score:.2f}</span>
  582. </div>
  583. '''
  584. if element_def:
  585. content += f'<div class="match-context"><strong>📖 定义:</strong> {html_module.escape(element_def)}</div>'
  586. if context_a:
  587. content += f'<div class="match-context"><strong>📍 所属:</strong> {html_module.escape(context_a)}</div>'
  588. # 显示B语义分析、匹配关系、A语义分析(三列布局)
  589. b_semantic = match_result.get("B语义分析", {})
  590. a_semantic = match_result.get("A语义分析", {})
  591. if b_semantic or a_semantic or match_relations:
  592. content += '<div class="semantic-analysis-with-relations">'
  593. # B语义分析
  594. if b_semantic:
  595. content += '''
  596. <div class="semantic-section b-semantic">
  597. <div class="semantic-header">💡 灵感点语义分析 (B)</div>
  598. <div class="semantic-content">
  599. '''
  600. b_substance = b_semantic.get("实质", {})
  601. b_form = b_semantic.get("形式", {})
  602. if b_substance:
  603. content += '<div class="semantic-part"><strong>实质:</strong></div>'
  604. for key, value in b_substance.items():
  605. content += f'''
  606. <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
  607. <span class="semantic-key">{html_module.escape(key)}:</span>
  608. <span class="semantic-value">{html_module.escape(value)}</span>
  609. </div>
  610. '''
  611. if b_form:
  612. content += '<div class="semantic-part"><strong>形式:</strong></div>'
  613. for key, value in b_form.items():
  614. content += f'''
  615. <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
  616. <span class="semantic-key">{html_module.escape(key)}:</span>
  617. <span class="semantic-value">{html_module.escape(value)}</span>
  618. </div>
  619. '''
  620. content += '''
  621. </div>
  622. </div>
  623. '''
  624. # 显示匹配关系(中间)
  625. if match_relations:
  626. content += '''
  627. <div class="match-relations-middle">
  628. <div class="relations-header">🔗 匹配关系</div>
  629. <div class="relations-content">
  630. '''
  631. for idx, rel in enumerate(match_relations):
  632. b_sem = rel.get("B语义", "")
  633. a_sem = rel.get("A语义", "")
  634. relation = rel.get("关系", "")
  635. explanation = rel.get("说明", "")
  636. distance_score = rel.get("距离分数", None)
  637. # 构建中间关系文本
  638. relation_middle = f'{html_module.escape(relation)}'
  639. if distance_score is not None:
  640. relation_middle += f' (分数: {distance_score:.2f})'
  641. # 为每个关系项生成唯一ID,用于连接线定位
  642. rel_id = f'rel_{index}_{idx}'
  643. b_sem_id = f'b_sem_{index}_{idx}'
  644. a_sem_id = f'a_sem_{index}_{idx}'
  645. content += f'''
  646. <div class="relation-item" id="{rel_id}">
  647. <div class="relation-badge">{relation_middle}</div>
  648. <div class="relation-semantics-ref">
  649. <div class="semantic-ref b-ref" data-semantic="{html_module.escape(b_sem)}">B: {html_module.escape(b_sem)}</div>
  650. <div class="relation-arrow">→</div>
  651. <div class="semantic-ref a-ref" data-semantic="{html_module.escape(a_sem)}">A: {html_module.escape(a_sem)}</div>
  652. </div>
  653. <div class="relation-explain">
  654. {html_module.escape(explanation)}
  655. </div>
  656. </div>
  657. '''
  658. content += '''
  659. </div>
  660. </div>
  661. '''
  662. # A语义分析
  663. if a_semantic:
  664. content += '''
  665. <div class="semantic-section a-semantic">
  666. <div class="semantic-header">🎯 人设要素语义分析 (A)</div>
  667. <div class="semantic-content">
  668. '''
  669. a_substance = a_semantic.get("实质", {})
  670. a_form = a_semantic.get("形式", {})
  671. if a_substance:
  672. content += '<div class="semantic-part"><strong>实质:</strong></div>'
  673. for key, value in a_substance.items():
  674. content += f'''
  675. <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
  676. <span class="semantic-key">{html_module.escape(key)}:</span>
  677. <span class="semantic-value">{html_module.escape(value)}</span>
  678. </div>
  679. '''
  680. if a_form:
  681. content += '<div class="semantic-part"><strong>形式:</strong></div>'
  682. for key, value in a_form.items():
  683. content += f'''
  684. <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
  685. <span class="semantic-key">{html_module.escape(key)}:</span>
  686. <span class="semantic-value">{html_module.escape(value)}</span>
  687. </div>
  688. '''
  689. content += '''
  690. </div>
  691. </div>
  692. '''
  693. content += '</div>' # end semantic-analysis-with-relations
  694. # 显示分数详情(放在最后)
  695. rule_score = match_result.get("规则分数", 0)
  696. rule_score_explain = match_result.get("规则分数说明", "")
  697. relevance_score = match_result.get("相关性分数", 0)
  698. relevance_explain = match_result.get("相关性说明", "")
  699. score_explain = match_result.get("score说明", "")
  700. if rule_score or relevance_score or score_explain:
  701. content += '<div class="score-calculation-section">'
  702. if rule_score or relevance_score:
  703. content += '<div class="score-details">'
  704. if rule_score:
  705. content += f'''
  706. <div class="score-detail-item rule-score">
  707. <div class="score-detail-header">
  708. <span class="score-detail-label">📐 规则分数</span>
  709. <span class="score-detail-value">{rule_score:.2f}</span>
  710. </div>
  711. {f'<div class="score-detail-explain">{html_module.escape(rule_score_explain)}</div>' if rule_score_explain else ''}
  712. </div>
  713. '''
  714. if relevance_score:
  715. content += f'''
  716. <div class="score-detail-item relevance-score">
  717. <div class="score-detail-header">
  718. <span class="score-detail-label">🎯 相关性分数</span>
  719. <span class="score-detail-value">{relevance_score:.2f}</span>
  720. </div>
  721. {f'<div class="score-detail-explain">{html_module.escape(relevance_explain)}</div>' if relevance_explain else ''}
  722. </div>
  723. '''
  724. content += '</div>'
  725. if score_explain:
  726. content += f'<div class="match-explain final-score"><strong>🎲 最终分数计算:</strong> {html_module.escape(score_explain)}</div>'
  727. content += '</div>'
  728. content += '''
  729. </div>
  730. '''
  731. content += '''
  732. </div>
  733. </div>
  734. '''
  735. content += '''
  736. </div>
  737. </div>
  738. '''
  739. # Step3 详细信息
  740. step3 = inspiration_data.get("step3")
  741. if step3:
  742. anchor_info = step3.get("锚点信息", {})
  743. step3_inspirations = step3.get("灵感点列表", [])
  744. anchor_category = anchor_info.get("锚点分类", "")
  745. category_def = anchor_info.get("分类定义", "")
  746. content += f'''
  747. <div class="modal-section">
  748. <h3>✨ Step3: 生成的灵感点</h3>
  749. <div class="step-content">
  750. <div class="step-field">
  751. <span class="step-field-label">🎯 锚点分类:</span>
  752. <span class="step-field-value">{html_module.escape(anchor_category)}</span>
  753. </div>
  754. '''
  755. if category_def:
  756. content += f'''
  757. <div class="step-field">
  758. <span class="step-field-label">📖 分类定义:</span>
  759. <span class="step-field-value">{html_module.escape(category_def)}</span>
  760. </div>
  761. '''
  762. if step3_inspirations:
  763. content += f'''
  764. <div class="step-field">
  765. <span class="step-field-label">生成的灵感点列表 (共 {len(step3_inspirations)} 个):</span>
  766. <div class="step3-inspirations-list">
  767. '''
  768. for idx, item in enumerate(step3_inspirations):
  769. path = item.get("推理路径", "")
  770. insp_point = item.get("灵感点", "")
  771. description = item.get("描述", "")
  772. content += f'''
  773. <div class="step3-inspiration-item">
  774. <div class="step3-header">
  775. <span class="step3-rank">#{idx + 1}</span>
  776. <span class="step3-point">{html_module.escape(insp_point)}</span>
  777. </div>
  778. <div class="step3-path"><strong>推理路径:</strong> {html_module.escape(path)}</div>
  779. <div class="step3-desc"><strong>描述:</strong> {html_module.escape(description)}</div>
  780. </div>
  781. '''
  782. content += '''
  783. </div>
  784. </div>
  785. '''
  786. content += '''
  787. </div>
  788. </div>
  789. '''
  790. # 日志链接
  791. if metadata.get("log_url"):
  792. content += f'''
  793. <div class="modal-link">
  794. <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
  795. 🔗 查看详细日志
  796. </a>
  797. </div>
  798. '''
  799. return content
  800. def generate_detail_modal_content_js() -> str:
  801. """
  802. 生成详情弹窗内容的JavaScript函数
  803. Returns:
  804. JavaScript代码字符串
  805. """
  806. return '''
  807. // Tab切换功能
  808. function switchTab(event, tabId) {
  809. // 移除所有tab的active状态
  810. const tabButtons = document.querySelectorAll('.tab-button');
  811. tabButtons.forEach(button => {
  812. button.classList.remove('active');
  813. });
  814. // 隐藏所有tab内容
  815. const tabContents = document.querySelectorAll('.tab-content');
  816. tabContents.forEach(content => {
  817. content.classList.remove('active');
  818. });
  819. // 激活当前tab
  820. event.currentTarget.classList.add('active');
  821. document.getElementById(tabId).classList.add('active');
  822. }
  823. function showInspirationDetail(element) {
  824. const detailHtml = element.dataset.detailHtml;
  825. const modal = document.getElementById('detailModal');
  826. const modalBody = document.getElementById('modalBody');
  827. modalBody.innerHTML = detailHtml;
  828. modal.classList.add('active');
  829. document.body.style.overflow = 'hidden';
  830. }
  831. function showStep1Detail(element) {
  832. const step1DetailHtml = element.dataset.step1DetailHtml;
  833. const modal = document.getElementById('detailModal');
  834. const modalBody = document.getElementById('modalBody');
  835. modalBody.innerHTML = step1DetailHtml;
  836. modal.classList.add('active');
  837. document.body.style.overflow = 'hidden';
  838. }
  839. function showStep3Detail(element) {
  840. const step3DetailHtml = element.dataset.step3DetailHtml;
  841. const modal = document.getElementById('detailModal');
  842. const modalBody = document.getElementById('modalBody');
  843. modalBody.innerHTML = step3DetailHtml;
  844. modal.classList.add('active');
  845. document.body.style.overflow = 'hidden';
  846. }
  847. function closeModal() {
  848. const modal = document.getElementById('detailModal');
  849. modal.classList.remove('active');
  850. document.body.style.overflow = '';
  851. }
  852. function closeModalOnOverlay(event) {
  853. if (event.target.id === 'detailModal') {
  854. closeModal();
  855. }
  856. }
  857. // ESC键关闭Modal
  858. document.addEventListener('keydown', function(event) {
  859. if (event.key === 'Escape') {
  860. closeModal();
  861. }
  862. });
  863. // 搜索和过滤功能
  864. function filterInspirations() {
  865. const searchInput = document.getElementById('searchInput').value.toLowerCase();
  866. const sortSelect = document.getElementById('sortSelect').value;
  867. const cards = document.querySelectorAll('.inspiration-card');
  868. let visibleCards = Array.from(cards);
  869. // 搜索过滤
  870. visibleCards.forEach(card => {
  871. const name = card.dataset.inspirationName.toLowerCase();
  872. if (name.includes(searchInput)) {
  873. card.style.display = '';
  874. } else {
  875. card.style.display = 'none';
  876. }
  877. });
  878. // 获取可见的卡片
  879. visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
  880. // 排序
  881. if (sortSelect === 'score-desc' || sortSelect === 'score-asc') {
  882. visibleCards.sort((a, b) => {
  883. const scoreA = parseFloat(a.dataset.step1Score) || 0;
  884. const scoreB = parseFloat(b.dataset.step1Score) || 0;
  885. return sortSelect === 'score-desc' ? scoreB - scoreA : scoreA - scoreB;
  886. });
  887. } else if (sortSelect === 'name-asc' || sortSelect === 'name-desc') {
  888. visibleCards.sort((a, b) => {
  889. const nameA = a.dataset.inspirationName;
  890. const nameB = b.dataset.inspirationName;
  891. return sortSelect === 'name-asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
  892. });
  893. }
  894. // 重新排列卡片
  895. const container = document.querySelector('.inspirations-grid');
  896. visibleCards.forEach(card => {
  897. container.appendChild(card);
  898. });
  899. // 更新统计
  900. updateStats();
  901. }
  902. function updateStats() {
  903. const cards = document.querySelectorAll('.inspiration-card');
  904. const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
  905. document.getElementById('totalCount').textContent = visibleCards.length;
  906. let excellentCount = 0;
  907. let goodCount = 0;
  908. let normalCount = 0;
  909. let needOptCount = 0;
  910. let totalScore = 0;
  911. visibleCards.forEach(card => {
  912. const score = parseFloat(card.dataset.step1Score) || 0;
  913. totalScore += score;
  914. // 分数统计
  915. if (score >= 0.7) excellentCount++;
  916. else if (score >= 0.5) goodCount++;
  917. else if (score >= 0.3) normalCount++;
  918. else needOptCount++;
  919. });
  920. document.getElementById('top1ExcellentCount').textContent = excellentCount;
  921. document.getElementById('top1GoodCount').textContent = goodCount;
  922. document.getElementById('top1NormalCount').textContent = normalCount;
  923. document.getElementById('top1NeedOptCount').textContent = needOptCount;
  924. const avgScore = visibleCards.length > 0 ? (totalScore / visibleCards.length).toFixed(3) : '0.000';
  925. document.getElementById('avgTop1Score').textContent = avgScore;
  926. }
  927. '''
  928. def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
  929. """
  930. 生成人设结构的树状HTML
  931. Args:
  932. persona_data: 人设数据
  933. Returns:
  934. 人设结构的HTML字符串
  935. """
  936. if not persona_data:
  937. return '<div class="empty-state">暂无人设数据</div>'
  938. inspiration_list = persona_data.get("灵感点列表", [])
  939. if not inspiration_list:
  940. return '<div class="empty-state">暂无灵感点列表数据</div>'
  941. html_parts = ['<div class="tree">']
  942. for perspective_idx, perspective in enumerate(inspiration_list):
  943. perspective_name = perspective.get("视角名称", "未知视角")
  944. perspective_desc = perspective.get("视角描述", "")
  945. pattern_list = perspective.get("模式列表", [])
  946. # 一级节点:视角
  947. html_parts.append(f'''
  948. <ul>
  949. <li>
  950. <div class="tree-node level-1">
  951. <span class="node-icon">📁</span>
  952. <span class="node-name">{html_module.escape(perspective_name)}</span>
  953. <span class="node-count">{len(pattern_list)}个分类</span>
  954. </div>
  955. ''')
  956. if perspective_desc:
  957. html_parts.append(f'''
  958. <div class="node-desc">{html_module.escape(perspective_desc)}</div>
  959. ''')
  960. # 二级节点:分类
  961. if pattern_list:
  962. html_parts.append('<ul>')
  963. for pattern in pattern_list:
  964. category_name = pattern.get("分类名称", "未知分类")
  965. core_definition = pattern.get("核心定义", "")
  966. subcategories = pattern.get("二级细分", [])
  967. total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
  968. html_parts.append(f'''
  969. <li>
  970. <div class="tree-node level-2">
  971. <span class="node-icon">📂</span>
  972. <span class="node-name">{html_module.escape(category_name)}</span>
  973. <span class="node-count">{total_posts}个帖子</span>
  974. </div>
  975. ''')
  976. if core_definition:
  977. html_parts.append(f'''
  978. <div class="node-desc">{html_module.escape(core_definition)}</div>
  979. ''')
  980. # 三级节点:细分
  981. if subcategories:
  982. html_parts.append('<ul>')
  983. for subcategory in subcategories:
  984. sub_name = subcategory.get("分类名称", "未知细分")
  985. sub_definition = subcategory.get("分类定义", "")
  986. post_ids = subcategory.get("帖子ID列表", [])
  987. html_parts.append(f'''
  988. <li>
  989. <div class="tree-node level-3">
  990. <span class="node-icon">📄</span>
  991. <span class="node-name">{html_module.escape(sub_name)}</span>
  992. <span class="node-count">{len(post_ids)}个帖子</span>
  993. </div>
  994. ''')
  995. if sub_definition:
  996. html_parts.append(f'''
  997. <div class="node-desc">{html_module.escape(sub_definition)}</div>
  998. ''')
  999. if post_ids:
  1000. html_parts.append(f'''
  1001. <div class="node-posts">
  1002. <span class="posts-label">📋 帖子ID:</span>
  1003. <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
  1004. {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
  1005. </div>
  1006. ''')
  1007. html_parts.append('</li>')
  1008. html_parts.append('</ul>')
  1009. html_parts.append('</li>')
  1010. html_parts.append('</ul>')
  1011. html_parts.append('</li>')
  1012. html_parts.append('</ul>')
  1013. html_parts.append('</div>')
  1014. return ''.join(html_parts)
  1015. def generate_html(
  1016. inspirations_data: List[Dict[str, Any]],
  1017. posts_map: Dict[str, Dict[str, Any]],
  1018. persona_data: Dict[str, Any],
  1019. output_path: str
  1020. ) -> str:
  1021. """
  1022. 生成完整的可视化HTML
  1023. Args:
  1024. inspirations_data: 灵感点数据列表
  1025. posts_map: 帖子数据映射
  1026. persona_data: 人设数据
  1027. output_path: 输出文件路径
  1028. Returns:
  1029. 输出文件路径
  1030. """
  1031. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  1032. # 统计信息
  1033. total_count = len(inspirations_data)
  1034. # 获取所有分数用于统计
  1035. def get_top1_score(d):
  1036. step1 = d.get("step1", {})
  1037. matches = step1.get("匹配结果列表", [])
  1038. if matches:
  1039. return matches[0].get("匹配结果", {}).get("score", 0)
  1040. return 0
  1041. # Top1分数统计
  1042. top1_excellent_count = sum(1 for d in inspirations_data if get_top1_score(d) >= 0.7)
  1043. top1_good_count = sum(1 for d in inspirations_data if 0.5 <= get_top1_score(d) < 0.7)
  1044. top1_normal_count = sum(1 for d in inspirations_data if 0.3 <= get_top1_score(d) < 0.5)
  1045. top1_need_opt_count = sum(1 for d in inspirations_data if get_top1_score(d) < 0.3)
  1046. # 平均分数
  1047. total_top1_score = sum(get_top1_score(d) for d in inspirations_data)
  1048. avg_top1_score = total_top1_score / total_count if total_count > 0 else 0
  1049. # 按Top1分数排序
  1050. inspirations_data_sorted = sorted(
  1051. inspirations_data,
  1052. key=lambda x: get_top1_score(x),
  1053. reverse=True
  1054. )
  1055. # 生成卡片HTML
  1056. cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted]
  1057. cards_html_str = '\n'.join(cards_html)
  1058. # 生成人设结构HTML
  1059. persona_structure_html = generate_persona_structure_html(persona_data)
  1060. # 生成JavaScript
  1061. detail_modal_js = generate_detail_modal_content_js()
  1062. # 完整HTML
  1063. html_content = f'''<!DOCTYPE html>
  1064. <html lang="zh-CN">
  1065. <head>
  1066. <meta charset="UTF-8">
  1067. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  1068. <title>灵感点分析可视化</title>
  1069. <style>
  1070. * {{
  1071. margin: 0;
  1072. padding: 0;
  1073. box-sizing: border-box;
  1074. }}
  1075. body {{
  1076. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  1077. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1078. color: #333;
  1079. line-height: 1.6;
  1080. min-height: 100vh;
  1081. padding: 20px;
  1082. }}
  1083. .container {{
  1084. max-width: 1600px;
  1085. margin: 0 auto;
  1086. }}
  1087. .header {{
  1088. background: white;
  1089. padding: 40px;
  1090. border-radius: 16px;
  1091. margin-bottom: 30px;
  1092. box-shadow: 0 10px 40px rgba(0,0,0,0.2);
  1093. }}
  1094. .header h1 {{
  1095. font-size: 42px;
  1096. margin-bottom: 10px;
  1097. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1098. -webkit-background-clip: text;
  1099. -webkit-text-fill-color: transparent;
  1100. font-weight: 800;
  1101. }}
  1102. .header-subtitle {{
  1103. font-size: 16px;
  1104. color: #6b7280;
  1105. margin-bottom: 30px;
  1106. }}
  1107. .stats-overview {{
  1108. display: grid;
  1109. grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  1110. gap: 20px;
  1111. margin-top: 25px;
  1112. }}
  1113. .stat-box {{
  1114. background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  1115. padding: 20px;
  1116. border-radius: 12px;
  1117. text-align: center;
  1118. transition: transform 0.3s ease;
  1119. }}
  1120. .stat-box:hover {{
  1121. transform: translateY(-5px);
  1122. }}
  1123. .stat-label {{
  1124. font-size: 13px;
  1125. color: #6b7280;
  1126. margin-bottom: 8px;
  1127. font-weight: 600;
  1128. }}
  1129. .stat-value {{
  1130. font-size: 32px;
  1131. font-weight: 700;
  1132. color: #1a1a1a;
  1133. }}
  1134. .stat-box.excellent .stat-value {{
  1135. color: #10b981;
  1136. }}
  1137. .stat-box.good .stat-value {{
  1138. color: #f59e0b;
  1139. }}
  1140. .stat-box.normal .stat-value {{
  1141. color: #3b82f6;
  1142. }}
  1143. .stat-box.need-opt .stat-value {{
  1144. color: #ef4444;
  1145. }}
  1146. .controls-section {{
  1147. background: #f9fafb;
  1148. padding: 25px;
  1149. border-radius: 12px;
  1150. margin-bottom: 30px;
  1151. display: flex;
  1152. gap: 20px;
  1153. flex-wrap: wrap;
  1154. align-items: center;
  1155. }}
  1156. .search-box {{
  1157. flex: 1;
  1158. min-width: 250px;
  1159. }}
  1160. .search-input {{
  1161. width: 100%;
  1162. padding: 12px 20px;
  1163. border: 2px solid #e5e7eb;
  1164. border-radius: 10px;
  1165. font-size: 15px;
  1166. transition: all 0.3s;
  1167. }}
  1168. .search-input:focus {{
  1169. outline: none;
  1170. border-color: #667eea;
  1171. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  1172. }}
  1173. .sort-box {{
  1174. display: flex;
  1175. align-items: center;
  1176. gap: 12px;
  1177. }}
  1178. .sort-label {{
  1179. font-size: 14px;
  1180. font-weight: 600;
  1181. color: #374151;
  1182. }}
  1183. .sort-select {{
  1184. padding: 10px 16px;
  1185. border: 2px solid #e5e7eb;
  1186. border-radius: 10px;
  1187. font-size: 14px;
  1188. background: white;
  1189. cursor: pointer;
  1190. transition: all 0.3s;
  1191. }}
  1192. .sort-select:focus {{
  1193. outline: none;
  1194. border-color: #667eea;
  1195. }}
  1196. .inspirations-section {{
  1197. padding: 0;
  1198. }}
  1199. .inspirations-grid {{
  1200. display: grid;
  1201. grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
  1202. gap: 25px;
  1203. }}
  1204. .inspiration-card {{
  1205. background: white;
  1206. border-radius: 14px;
  1207. padding: 25px;
  1208. border-left: 6px solid #10b981;
  1209. cursor: pointer;
  1210. transition: all 0.3s ease;
  1211. box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  1212. position: relative;
  1213. }}
  1214. .inspiration-card:hover {{
  1215. transform: translateY(-8px);
  1216. box-shadow: 0 12px 30px rgba(102, 126, 234, 0.2);
  1217. }}
  1218. .card-header {{
  1219. display: flex;
  1220. justify-content: space-between;
  1221. align-items: flex-start;
  1222. margin-bottom: 20px;
  1223. gap: 12px;
  1224. }}
  1225. .inspiration-name {{
  1226. font-size: 19px;
  1227. font-weight: 700;
  1228. color: #1a1a1a;
  1229. line-height: 1.4;
  1230. flex: 1;
  1231. }}
  1232. .grade-badge {{
  1233. background: #10b981;
  1234. color: white;
  1235. padding: 6px 14px;
  1236. border-radius: 20px;
  1237. font-size: 12px;
  1238. font-weight: 700;
  1239. white-space: nowrap;
  1240. }}
  1241. .score-section {{
  1242. display: flex;
  1243. align-items: center;
  1244. gap: 25px;
  1245. margin-bottom: 20px;
  1246. padding: 20px;
  1247. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  1248. border-radius: 12px;
  1249. }}
  1250. .score-item {{
  1251. display: flex;
  1252. flex-direction: column;
  1253. align-items: center;
  1254. gap: 8px;
  1255. flex: 1;
  1256. }}
  1257. .main-score {{
  1258. display: flex;
  1259. flex-direction: column;
  1260. align-items: center;
  1261. gap: 8px;
  1262. }}
  1263. .score-circle {{
  1264. width: 90px;
  1265. height: 90px;
  1266. border-radius: 50%;
  1267. border: 6px solid #10b981;
  1268. display: flex;
  1269. align-items: center;
  1270. justify-content: center;
  1271. background: white;
  1272. }}
  1273. .score-value {{
  1274. font-size: 26px;
  1275. font-weight: 800;
  1276. color: #10b981;
  1277. }}
  1278. .score-label {{
  1279. font-size: 12px;
  1280. color: #6b7280;
  1281. font-weight: 600;
  1282. }}
  1283. .score-match-element {{
  1284. font-size: 13px;
  1285. color: #374151;
  1286. font-weight: 600;
  1287. text-align: center;
  1288. padding: 4px 8px;
  1289. background: white;
  1290. border-radius: 6px;
  1291. }}
  1292. .sub-scores {{
  1293. flex: 1;
  1294. display: flex;
  1295. flex-direction: column;
  1296. gap: 12px;
  1297. }}
  1298. .sub-score-item {{
  1299. display: flex;
  1300. justify-content: space-between;
  1301. align-items: center;
  1302. padding: 10px 15px;
  1303. background: white;
  1304. border-radius: 8px;
  1305. }}
  1306. .sub-score-label {{
  1307. font-size: 13px;
  1308. color: #6b7280;
  1309. font-weight: 600;
  1310. }}
  1311. .sub-score-value {{
  1312. font-size: 18px;
  1313. font-weight: 700;
  1314. color: #2563eb;
  1315. }}
  1316. .metrics-section {{
  1317. display: flex;
  1318. flex-direction: column;
  1319. gap: 10px;
  1320. margin-bottom: 15px;
  1321. }}
  1322. .metric-item {{
  1323. display: flex;
  1324. align-items: center;
  1325. gap: 8px;
  1326. font-size: 13px;
  1327. color: #4b5563;
  1328. }}
  1329. .metric-icon {{
  1330. font-size: 16px;
  1331. }}
  1332. .metric-label {{
  1333. font-weight: 600;
  1334. }}
  1335. .metric-value {{
  1336. color: #1f2937;
  1337. }}
  1338. .match-preview {{
  1339. background: #f9fafb;
  1340. padding: 12px;
  1341. border-radius: 8px;
  1342. margin-bottom: 10px;
  1343. border-left: 3px solid #8b5cf6;
  1344. }}
  1345. .match-preview-header {{
  1346. font-size: 12px;
  1347. font-weight: 600;
  1348. color: #6b7280;
  1349. margin-bottom: 6px;
  1350. }}
  1351. .match-preview-content {{
  1352. display: flex;
  1353. justify-content: space-between;
  1354. align-items: center;
  1355. }}
  1356. .match-preview-name {{
  1357. font-size: 13px;
  1358. color: #1f2937;
  1359. flex: 1;
  1360. }}
  1361. .match-preview-score {{
  1362. font-size: 16px;
  1363. font-weight: 700;
  1364. }}
  1365. .preview-parts {{
  1366. margin-top: 8px;
  1367. padding: 8px 10px;
  1368. border-radius: 6px;
  1369. font-size: 12px;
  1370. line-height: 1.6;
  1371. }}
  1372. .preview-parts.same {{
  1373. background: #f0fdf4;
  1374. color: #15803d;
  1375. border-left: 3px solid #10b981;
  1376. }}
  1377. .preview-parts.increment {{
  1378. background: #fff7ed;
  1379. color: #92400e;
  1380. border-left: 3px solid #f59e0b;
  1381. margin-top: 6px;
  1382. }}
  1383. .preview-parts strong {{
  1384. font-weight: 700;
  1385. margin-right: 6px;
  1386. }}
  1387. .score-divider {{
  1388. width: 1px;
  1389. height: 40px;
  1390. background: #e5e7eb;
  1391. }}
  1392. .click-hint {{
  1393. position: absolute;
  1394. bottom: 15px;
  1395. right: 15px;
  1396. font-size: 12px;
  1397. color: #8b5cf6;
  1398. font-weight: 700;
  1399. opacity: 0;
  1400. transition: opacity 0.3s ease;
  1401. background: rgba(139, 92, 246, 0.1);
  1402. padding: 6px 12px;
  1403. border-radius: 8px;
  1404. }}
  1405. .inspiration-card:hover .click-hint {{
  1406. opacity: 1;
  1407. }}
  1408. /* Modal样式 */
  1409. .modal-overlay {{
  1410. display: none;
  1411. position: fixed;
  1412. top: 0;
  1413. left: 0;
  1414. right: 0;
  1415. bottom: 0;
  1416. background: rgba(0, 0, 0, 0.8);
  1417. z-index: 1000;
  1418. align-items: center;
  1419. justify-content: center;
  1420. padding: 20px;
  1421. overflow-y: auto;
  1422. }}
  1423. .modal-overlay.active {{
  1424. display: flex;
  1425. }}
  1426. .modal-content {{
  1427. background: white;
  1428. border-radius: 16px;
  1429. max-width: 1200px;
  1430. width: 100%;
  1431. max-height: 90vh;
  1432. overflow-y: auto;
  1433. position: relative;
  1434. }}
  1435. .modal-close {{
  1436. position: sticky;
  1437. top: 0;
  1438. right: 0;
  1439. background: white;
  1440. border: none;
  1441. font-size: 32px;
  1442. color: #6b7280;
  1443. cursor: pointer;
  1444. padding: 15px 20px;
  1445. z-index: 10;
  1446. text-align: right;
  1447. border-bottom: 1px solid #e5e7eb;
  1448. }}
  1449. .modal-close:hover {{
  1450. color: #1f2937;
  1451. }}
  1452. .modal-body {{
  1453. padding: 30px;
  1454. }}
  1455. .modal-header {{
  1456. margin-bottom: 25px;
  1457. padding-bottom: 20px;
  1458. border-bottom: 2px solid #e5e7eb;
  1459. }}
  1460. .modal-title {{
  1461. font-size: 28px;
  1462. font-weight: 800;
  1463. color: #1a1a1a;
  1464. }}
  1465. .modal-section {{
  1466. margin-bottom: 30px;
  1467. }}
  1468. .modal-section h3 {{
  1469. font-size: 20px;
  1470. font-weight: 700;
  1471. color: #374151;
  1472. margin-bottom: 15px;
  1473. padding-bottom: 10px;
  1474. border-bottom: 2px solid #f3f4f6;
  1475. }}
  1476. .info-grid {{
  1477. display: grid;
  1478. grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  1479. gap: 15px;
  1480. }}
  1481. .info-item {{
  1482. background: #f9fafb;
  1483. padding: 12px 16px;
  1484. border-radius: 8px;
  1485. border-left: 3px solid #8b5cf6;
  1486. }}
  1487. .info-label {{
  1488. font-weight: 600;
  1489. color: #6b7280;
  1490. font-size: 13px;
  1491. margin-right: 8px;
  1492. }}
  1493. .info-value {{
  1494. color: #1f2937;
  1495. font-size: 14px;
  1496. }}
  1497. .metrics-grid {{
  1498. display: grid;
  1499. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  1500. gap: 15px;
  1501. }}
  1502. .metric-box {{
  1503. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1504. padding: 20px;
  1505. border-radius: 12px;
  1506. text-align: center;
  1507. color: white;
  1508. }}
  1509. .metric-box.wide {{
  1510. grid-column: span 2;
  1511. }}
  1512. .metric-box-label {{
  1513. font-size: 13px;
  1514. opacity: 0.9;
  1515. margin-bottom: 8px;
  1516. font-weight: 600;
  1517. }}
  1518. .metric-box-value {{
  1519. font-size: 28px;
  1520. font-weight: 700;
  1521. }}
  1522. .metric-box-value.small {{
  1523. font-size: 16px;
  1524. }}
  1525. .step-content {{
  1526. background: #f9fafb;
  1527. padding: 20px;
  1528. border-radius: 12px;
  1529. }}
  1530. .step-field {{
  1531. margin-bottom: 20px;
  1532. }}
  1533. .step-field-label {{
  1534. font-weight: 700;
  1535. color: #374151;
  1536. font-size: 14px;
  1537. margin-bottom: 8px;
  1538. display: block;
  1539. }}
  1540. .step-field-value {{
  1541. color: #1f2937;
  1542. font-size: 15px;
  1543. line-height: 1.7;
  1544. }}
  1545. .matches-list {{
  1546. display: flex;
  1547. flex-direction: column;
  1548. gap: 15px;
  1549. margin-top: 10px;
  1550. }}
  1551. .match-item {{
  1552. background: white;
  1553. padding: 18px;
  1554. border-radius: 10px;
  1555. border-left: 5px solid #3b82f6;
  1556. }}
  1557. .match-item.top1 {{
  1558. border-left-color: #fbbf24;
  1559. background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
  1560. }}
  1561. .match-item.top2 {{
  1562. border-left-color: #c0c0c0;
  1563. background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
  1564. }}
  1565. .match-item.top3 {{
  1566. border-left-color: #cd7f32;
  1567. background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
  1568. }}
  1569. .match-header {{
  1570. display: flex;
  1571. justify-content: space-between;
  1572. align-items: center;
  1573. margin-bottom: 12px;
  1574. gap: 10px;
  1575. }}
  1576. .match-rank {{
  1577. font-size: 18px;
  1578. font-weight: 800;
  1579. color: #6b7280;
  1580. }}
  1581. .match-element-name {{
  1582. flex: 1;
  1583. font-size: 16px;
  1584. font-weight: 700;
  1585. color: #1f2937;
  1586. }}
  1587. .match-score {{
  1588. font-size: 22px;
  1589. font-weight: 800;
  1590. color: #2563eb;
  1591. background: white;
  1592. padding: 6px 14px;
  1593. border-radius: 8px;
  1594. }}
  1595. .match-detail {{
  1596. background: rgba(255, 255, 255, 0.7);
  1597. padding: 10px;
  1598. border-radius: 6px;
  1599. margin-bottom: 10px;
  1600. font-size: 13px;
  1601. color: #4b5563;
  1602. }}
  1603. .match-reason {{
  1604. color: #1f2937;
  1605. font-size: 14px;
  1606. line-height: 1.7;
  1607. }}
  1608. .increment-matches {{
  1609. display: flex;
  1610. flex-direction: column;
  1611. gap: 12px;
  1612. margin-top: 10px;
  1613. }}
  1614. .increment-item {{
  1615. background: white;
  1616. padding: 15px;
  1617. border-radius: 8px;
  1618. border-left: 4px solid #10b981;
  1619. }}
  1620. .increment-header {{
  1621. display: flex;
  1622. justify-content: space-between;
  1623. align-items: center;
  1624. margin-bottom: 10px;
  1625. }}
  1626. .increment-words {{
  1627. font-weight: 700;
  1628. color: #1f2937;
  1629. font-size: 15px;
  1630. }}
  1631. .increment-score {{
  1632. font-size: 20px;
  1633. font-weight: 800;
  1634. color: #10b981;
  1635. }}
  1636. .increment-reason {{
  1637. color: #4b5563;
  1638. font-size: 13px;
  1639. line-height: 1.6;
  1640. }}
  1641. .empty-state {{
  1642. text-align: center;
  1643. padding: 40px;
  1644. color: #9ca3af;
  1645. font-size: 14px;
  1646. }}
  1647. .modal-link {{
  1648. margin-top: 25px;
  1649. padding-top: 20px;
  1650. border-top: 2px solid #e5e7eb;
  1651. text-align: center;
  1652. }}
  1653. .modal-link-btn {{
  1654. display: inline-flex;
  1655. align-items: center;
  1656. gap: 10px;
  1657. padding: 12px 24px;
  1658. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1659. color: white;
  1660. text-decoration: none;
  1661. border-radius: 10px;
  1662. font-size: 15px;
  1663. font-weight: 600;
  1664. transition: all 0.3s;
  1665. }}
  1666. .modal-link-btn:hover {{
  1667. transform: translateY(-2px);
  1668. box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
  1669. }}
  1670. .timestamp {{
  1671. text-align: center;
  1672. color: white;
  1673. font-size: 13px;
  1674. margin-top: 30px;
  1675. opacity: 0.8;
  1676. }}
  1677. .match-context {{
  1678. background: #f3f4f6;
  1679. padding: 8px 12px;
  1680. border-radius: 6px;
  1681. margin: 8px 0;
  1682. font-size: 12px;
  1683. color: #6b7280;
  1684. line-height: 1.6;
  1685. }}
  1686. .match-explain {{
  1687. background: #fef3c7;
  1688. padding: 10px 12px;
  1689. border-radius: 6px;
  1690. margin: 10px 0;
  1691. font-size: 13px;
  1692. color: #92400e;
  1693. line-height: 1.7;
  1694. border-left: 3px solid #f59e0b;
  1695. }}
  1696. .match-parts {{
  1697. margin: 12px 0;
  1698. border-radius: 8px;
  1699. overflow: hidden;
  1700. }}
  1701. .match-parts.same-parts {{
  1702. background: #f0fdf4;
  1703. border: 2px solid #10b981;
  1704. }}
  1705. .match-parts.increment-parts {{
  1706. background: #fff7ed;
  1707. border: 2px solid #f59e0b;
  1708. }}
  1709. .parts-header {{
  1710. font-weight: 700;
  1711. padding: 10px 12px;
  1712. font-size: 13px;
  1713. }}
  1714. .same-parts .parts-header {{
  1715. background: #dcfce7;
  1716. color: #15803d;
  1717. }}
  1718. .increment-parts .parts-header {{
  1719. background: #fed7aa;
  1720. color: #92400e;
  1721. }}
  1722. .parts-content {{
  1723. padding: 8px 12px;
  1724. }}
  1725. .part-item {{
  1726. padding: 6px 0;
  1727. border-bottom: 1px solid rgba(0,0,0,0.05);
  1728. font-size: 13px;
  1729. line-height: 1.6;
  1730. }}
  1731. .part-item:last-child {{
  1732. border-bottom: none;
  1733. }}
  1734. .part-key {{
  1735. font-weight: 600;
  1736. color: #374151;
  1737. margin-right: 6px;
  1738. }}
  1739. .part-value {{
  1740. color: #1f2937;
  1741. }}
  1742. .increment-context {{
  1743. background: #fef3c7;
  1744. padding: 10px 12px;
  1745. border-radius: 6px;
  1746. margin: 10px 0;
  1747. font-size: 12px;
  1748. color: #92400e;
  1749. line-height: 1.6;
  1750. border-left: 3px solid #f59e0b;
  1751. }}
  1752. /* 语义分析样式 */
  1753. .semantic-analysis {{
  1754. margin: 15px 0;
  1755. display: grid;
  1756. grid-template-columns: 1fr 1fr;
  1757. gap: 15px;
  1758. }}
  1759. /* 三列布局:B语义、匹配关系、A语义 */
  1760. .semantic-analysis-with-relations {{
  1761. margin: 15px 0;
  1762. display: grid;
  1763. grid-template-columns: 1fr auto 1fr;
  1764. gap: 20px;
  1765. align-items: start;
  1766. }}
  1767. .semantic-section {{
  1768. border-radius: 8px;
  1769. overflow: hidden;
  1770. border: 2px solid;
  1771. }}
  1772. .semantic-section.b-semantic {{
  1773. background: #fffbeb;
  1774. border-color: #f59e0b;
  1775. }}
  1776. .semantic-section.a-semantic {{
  1777. background: #f0fdf4;
  1778. border-color: #10b981;
  1779. }}
  1780. .semantic-header {{
  1781. font-weight: 700;
  1782. padding: 10px 12px;
  1783. font-size: 13px;
  1784. }}
  1785. .b-semantic .semantic-header {{
  1786. background: #fef3c7;
  1787. color: #92400e;
  1788. }}
  1789. .a-semantic .semantic-header {{
  1790. background: #dcfce7;
  1791. color: #15803d;
  1792. }}
  1793. .semantic-content {{
  1794. padding: 10px 12px;
  1795. }}
  1796. .semantic-part {{
  1797. font-weight: 600;
  1798. font-size: 12px;
  1799. color: #6b7280;
  1800. margin: 10px 0 6px 0;
  1801. }}
  1802. .semantic-part:first-child {{
  1803. margin-top: 0;
  1804. }}
  1805. .semantic-item {{
  1806. padding: 8px 10px;
  1807. margin: 6px 0;
  1808. border-radius: 6px;
  1809. font-size: 12px;
  1810. line-height: 1.6;
  1811. }}
  1812. .semantic-item.substance {{
  1813. background: rgba(255, 255, 255, 0.7);
  1814. border-left: 3px solid #f59e0b;
  1815. }}
  1816. .semantic-item.form {{
  1817. background: rgba(255, 255, 255, 0.5);
  1818. border-left: 3px solid #94a3b8;
  1819. }}
  1820. .semantic-key {{
  1821. font-weight: 700;
  1822. color: #374151;
  1823. margin-right: 6px;
  1824. }}
  1825. .semantic-value {{
  1826. color: #1f2937;
  1827. }}
  1828. @media (max-width: 1024px) {{
  1829. .semantic-analysis {{
  1830. grid-template-columns: 1fr;
  1831. }}
  1832. .semantic-analysis-with-relations {{
  1833. grid-template-columns: 1fr;
  1834. }}
  1835. }}
  1836. /* 中间匹配关系列样式 */
  1837. .match-relations-middle {{
  1838. display: flex;
  1839. flex-direction: column;
  1840. justify-content: center;
  1841. min-width: 180px;
  1842. max-width: 250px;
  1843. position: relative;
  1844. }}
  1845. .match-relations-middle .relations-header {{
  1846. background: #dbeafe;
  1847. color: #1e40af;
  1848. font-weight: 700;
  1849. padding: 8px 12px;
  1850. font-size: 12px;
  1851. text-align: center;
  1852. border-radius: 6px;
  1853. margin-bottom: 12px;
  1854. }}
  1855. .match-relations-middle .relations-content {{
  1856. flex: 1;
  1857. display: flex;
  1858. flex-direction: column;
  1859. gap: 20px;
  1860. padding: 15px 10px;
  1861. position: relative;
  1862. }}
  1863. .relation-item {{
  1864. display: flex;
  1865. flex-direction: column;
  1866. gap: 12px;
  1867. align-items: center;
  1868. }}
  1869. .relation-badge {{
  1870. background: #e0f2fe;
  1871. color: #0c4a6e;
  1872. padding: 12px 16px;
  1873. border-radius: 8px;
  1874. font-weight: 700;
  1875. font-size: 14px;
  1876. border: 2px solid #3b82f6;
  1877. box-shadow: 0 2px 6px rgba(0,0,0,0.15);
  1878. text-align: center;
  1879. white-space: nowrap;
  1880. }}
  1881. .relation-semantics-ref {{
  1882. display: flex;
  1883. align-items: center;
  1884. gap: 10px;
  1885. width: 100%;
  1886. justify-content: center;
  1887. flex-wrap: wrap;
  1888. }}
  1889. .semantic-ref {{
  1890. padding: 6px 12px;
  1891. border-radius: 6px;
  1892. font-size: 11px;
  1893. font-weight: 600;
  1894. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  1895. }}
  1896. .semantic-ref.b-ref {{
  1897. background: #fef3c7;
  1898. color: #92400e;
  1899. border: 1px solid #f59e0b;
  1900. }}
  1901. .semantic-ref.a-ref {{
  1902. background: #dcfce7;
  1903. color: #15803d;
  1904. border: 1px solid #10b981;
  1905. }}
  1906. .relation-semantics-ref .relation-arrow {{
  1907. font-size: 16px;
  1908. color: #ef4444;
  1909. font-weight: 900;
  1910. }}
  1911. .match-relations-middle .relation-explain {{
  1912. font-size: 12px;
  1913. color: #374151;
  1914. line-height: 1.6;
  1915. padding: 12px;
  1916. background: rgba(255, 255, 255, 0.95);
  1917. border-radius: 6px;
  1918. border: 1px solid #cbd5e1;
  1919. width: 100%;
  1920. text-align: left;
  1921. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  1922. }}
  1923. /* 当鼠标悬停在语义引用上时,高亮对应的左右语义项 */
  1924. .semantic-ref:hover {{
  1925. box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
  1926. transform: scale(1.05);
  1927. cursor: pointer;
  1928. transition: all 0.2s;
  1929. }}
  1930. /* 匹配关系样式(旧的,保留用于其他地方) */
  1931. .match-relations {{
  1932. margin: 12px 0;
  1933. background: #f0f9ff;
  1934. border-radius: 8px;
  1935. border: 2px solid #3b82f6;
  1936. overflow: hidden;
  1937. }}
  1938. .relations-header {{
  1939. background: #dbeafe;
  1940. color: #1e40af;
  1941. font-weight: 700;
  1942. padding: 10px 12px;
  1943. font-size: 13px;
  1944. }}
  1945. .relations-content {{
  1946. padding: 8px 12px;
  1947. }}
  1948. /* 分数计算区域样式 */
  1949. .score-calculation-section {{
  1950. margin-top: 20px;
  1951. padding-top: 20px;
  1952. border-top: 2px solid #e5e7eb;
  1953. }}
  1954. /* 分数详情样式 */
  1955. .score-details {{
  1956. display: grid;
  1957. grid-template-columns: 1fr 1fr;
  1958. gap: 12px;
  1959. margin: 15px 0;
  1960. }}
  1961. .score-detail-item {{
  1962. border-radius: 8px;
  1963. padding: 12px;
  1964. border: 2px solid;
  1965. }}
  1966. .score-detail-item.rule-score {{
  1967. background: #fef3c7;
  1968. border-color: #f59e0b;
  1969. }}
  1970. .score-detail-item.relevance-score {{
  1971. background: #e0f2fe;
  1972. border-color: #3b82f6;
  1973. }}
  1974. .score-detail-header {{
  1975. display: flex;
  1976. justify-content: space-between;
  1977. align-items: center;
  1978. margin-bottom: 8px;
  1979. }}
  1980. .score-detail-label {{
  1981. font-weight: 700;
  1982. font-size: 13px;
  1983. }}
  1984. .rule-score .score-detail-label {{
  1985. color: #92400e;
  1986. }}
  1987. .relevance-score .score-detail-label {{
  1988. color: #1e40af;
  1989. }}
  1990. .score-detail-value {{
  1991. font-size: 20px;
  1992. font-weight: 800;
  1993. }}
  1994. .rule-score .score-detail-value {{
  1995. color: #f59e0b;
  1996. }}
  1997. .relevance-score .score-detail-value {{
  1998. color: #3b82f6;
  1999. }}
  2000. .score-detail-explain {{
  2001. font-size: 12px;
  2002. line-height: 1.6;
  2003. color: #374151;
  2004. }}
  2005. @media (max-width: 768px) {{
  2006. .score-details {{
  2007. grid-template-columns: 1fr;
  2008. }}
  2009. }}
  2010. /* 匹配预览列表样式 */
  2011. .match-preview-list {{
  2012. display: flex;
  2013. flex-direction: column;
  2014. gap: 8px;
  2015. }}
  2016. .preview-match-item {{
  2017. display: flex;
  2018. align-items: center;
  2019. gap: 10px;
  2020. padding: 8px 12px;
  2021. background: white;
  2022. border-radius: 6px;
  2023. border-left: 3px solid #e5e7eb;
  2024. }}
  2025. .preview-rank {{
  2026. font-size: 16px;
  2027. font-weight: 800;
  2028. }}
  2029. .preview-name {{
  2030. flex: 1;
  2031. font-size: 13px;
  2032. font-weight: 600;
  2033. color: #374151;
  2034. }}
  2035. .preview-score {{
  2036. font-size: 15px;
  2037. font-weight: 800;
  2038. }}
  2039. /* Tab样式 */
  2040. .tabs-nav {{
  2041. background: white;
  2042. padding: 0 30px;
  2043. border-radius: 16px 16px 0 0;
  2044. margin-bottom: 0;
  2045. box-shadow: 0 4px 20px rgba(0,0,0,0.1);
  2046. display: flex;
  2047. gap: 10px;
  2048. }}
  2049. .tab-button {{
  2050. padding: 15px 30px;
  2051. border: none;
  2052. background: transparent;
  2053. color: #6b7280;
  2054. font-size: 15px;
  2055. font-weight: 600;
  2056. cursor: pointer;
  2057. border-bottom: 3px solid transparent;
  2058. transition: all 0.3s;
  2059. }}
  2060. .tab-button:hover {{
  2061. color: #667eea;
  2062. background: rgba(102, 126, 234, 0.05);
  2063. }}
  2064. .tab-button.active {{
  2065. color: #667eea;
  2066. border-bottom-color: #667eea;
  2067. background: rgba(102, 126, 234, 0.05);
  2068. }}
  2069. .tab-content {{
  2070. display: none;
  2071. background: white;
  2072. padding: 30px;
  2073. border-radius: 0 0 16px 16px;
  2074. box-shadow: 0 10px 40px rgba(0,0,0,0.15);
  2075. }}
  2076. .tab-content.active {{
  2077. display: block;
  2078. }}
  2079. /* 人设结构样式 */
  2080. .persona-structure-section h2 {{
  2081. font-size: 28px;
  2082. font-weight: 700;
  2083. margin-bottom: 25px;
  2084. color: #1a1a1a;
  2085. }}
  2086. /* 树状图样式 */
  2087. .tree {{
  2088. font-size: 14px;
  2089. }}
  2090. .tree ul {{
  2091. padding-left: 30px;
  2092. list-style: none;
  2093. position: relative;
  2094. }}
  2095. .tree ul ul {{
  2096. padding-left: 40px;
  2097. }}
  2098. .tree li {{
  2099. position: relative;
  2100. padding: 8px 0;
  2101. }}
  2102. .tree li::before {{
  2103. content: "";
  2104. position: absolute;
  2105. top: 0;
  2106. left: -20px;
  2107. border-left: 2px solid #d1d5db;
  2108. border-bottom: 2px solid #d1d5db;
  2109. width: 20px;
  2110. height: 20px;
  2111. }}
  2112. .tree li::after {{
  2113. content: "";
  2114. position: absolute;
  2115. top: 20px;
  2116. left: -20px;
  2117. border-left: 2px solid #d1d5db;
  2118. height: 100%;
  2119. }}
  2120. .tree li:last-child::after {{
  2121. display: none;
  2122. }}
  2123. .tree > ul > li::before,
  2124. .tree > ul > li::after {{
  2125. display: none;
  2126. }}
  2127. .tree-node {{
  2128. display: inline-flex;
  2129. align-items: center;
  2130. gap: 10px;
  2131. padding: 12px 16px;
  2132. border-radius: 8px;
  2133. transition: all 0.3s;
  2134. margin-bottom: 8px;
  2135. }}
  2136. .tree-node:hover {{
  2137. transform: translateX(4px);
  2138. box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  2139. }}
  2140. .tree-node.level-1 {{
  2141. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2142. color: white;
  2143. font-size: 18px;
  2144. font-weight: 700;
  2145. padding: 16px 20px;
  2146. }}
  2147. .tree-node.level-2 {{
  2148. background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
  2149. color: #1e40af;
  2150. font-size: 16px;
  2151. font-weight: 600;
  2152. border: 2px solid #3b82f6;
  2153. }}
  2154. .tree-node.level-3 {{
  2155. background: white;
  2156. color: #374151;
  2157. font-size: 14px;
  2158. font-weight: 500;
  2159. border: 1px solid #e5e7eb;
  2160. }}
  2161. .node-icon {{
  2162. font-size: 20px;
  2163. }}
  2164. .tree-node.level-1 .node-icon {{
  2165. font-size: 24px;
  2166. }}
  2167. .node-name {{
  2168. flex: 1;
  2169. }}
  2170. .node-count {{
  2171. background: rgba(255, 255, 255, 0.3);
  2172. padding: 4px 12px;
  2173. border-radius: 12px;
  2174. font-size: 12px;
  2175. font-weight: 600;
  2176. }}
  2177. .tree-node.level-1 .node-count {{
  2178. background: rgba(255, 255, 255, 0.4);
  2179. }}
  2180. .tree-node.level-2 .node-count {{
  2181. background: #bfdbfe;
  2182. color: #1e3a8a;
  2183. }}
  2184. .tree-node.level-3 .node-count {{
  2185. background: #dcfce7;
  2186. color: #166534;
  2187. }}
  2188. .node-desc {{
  2189. margin: 8px 0 8px 50px;
  2190. padding: 12px 16px;
  2191. background: #fffbeb;
  2192. border-left: 3px solid #f59e0b;
  2193. border-radius: 6px;
  2194. color: #92400e;
  2195. font-size: 13px;
  2196. line-height: 1.7;
  2197. }}
  2198. .node-posts {{
  2199. margin: 8px 0 8px 50px;
  2200. padding: 12px 16px;
  2201. background: #f0fdf4;
  2202. border-left: 3px solid #10b981;
  2203. border-radius: 6px;
  2204. font-size: 12px;
  2205. line-height: 1.8;
  2206. }}
  2207. .posts-label {{
  2208. font-weight: 600;
  2209. color: #15803d;
  2210. margin-right: 8px;
  2211. }}
  2212. .posts-ids {{
  2213. color: #166534;
  2214. word-break: break-all;
  2215. }}
  2216. .posts-more {{
  2217. color: #059669;
  2218. font-weight: 600;
  2219. margin-left: 8px;
  2220. }}
  2221. @media (max-width: 768px) {{
  2222. .inspirations-grid {{
  2223. grid-template-columns: 1fr;
  2224. }}
  2225. .header h1 {{
  2226. font-size: 32px;
  2227. }}
  2228. .stats-overview {{
  2229. grid-template-columns: repeat(2, 1fr);
  2230. }}
  2231. }}
  2232. /* Step3 预览样式 */
  2233. .step3-preview {{
  2234. background: #fef3c7;
  2235. padding: 12px;
  2236. border-radius: 8px;
  2237. margin-bottom: 10px;
  2238. border-left: 3px solid #f59e0b;
  2239. }}
  2240. .step3-preview-header {{
  2241. font-size: 12px;
  2242. font-weight: 600;
  2243. color: #92400e;
  2244. margin-bottom: 8px;
  2245. }}
  2246. .step3-preview-list {{
  2247. display: flex;
  2248. flex-direction: column;
  2249. gap: 8px;
  2250. }}
  2251. .step3-preview-item {{
  2252. background: white;
  2253. padding: 10px;
  2254. border-radius: 6px;
  2255. display: flex;
  2256. flex-direction: column;
  2257. gap: 6px;
  2258. }}
  2259. .step3-point {{
  2260. font-size: 13px;
  2261. font-weight: 700;
  2262. color: #1f2937;
  2263. }}
  2264. .step3-path {{
  2265. font-size: 11px;
  2266. color: #6b7280;
  2267. line-height: 1.5;
  2268. }}
  2269. /* Step3 详情样式 */
  2270. .step3-inspirations-list {{
  2271. display: flex;
  2272. flex-direction: column;
  2273. gap: 12px;
  2274. margin-top: 10px;
  2275. }}
  2276. .step3-inspiration-item {{
  2277. background: white;
  2278. padding: 15px;
  2279. border-radius: 8px;
  2280. border-left: 4px solid #f59e0b;
  2281. }}
  2282. .step3-header {{
  2283. display: flex;
  2284. align-items: center;
  2285. gap: 10px;
  2286. margin-bottom: 10px;
  2287. }}
  2288. .step3-rank {{
  2289. font-size: 16px;
  2290. font-weight: 800;
  2291. color: #92400e;
  2292. background: #fef3c7;
  2293. padding: 4px 10px;
  2294. border-radius: 6px;
  2295. }}
  2296. .step3-inspiration-item .step3-point {{
  2297. font-size: 16px;
  2298. font-weight: 700;
  2299. color: #1f2937;
  2300. flex: 1;
  2301. }}
  2302. .step3-inspiration-item .step3-path {{
  2303. background: #f9fafb;
  2304. padding: 8px 10px;
  2305. border-radius: 6px;
  2306. font-size: 12px;
  2307. color: #4b5563;
  2308. margin: 8px 0;
  2309. }}
  2310. .step3-desc {{
  2311. font-size: 13px;
  2312. color: #374151;
  2313. line-height: 1.7;
  2314. }}
  2315. /* 卡片操作按钮样式 */
  2316. .card-actions {{
  2317. display: flex;
  2318. gap: 10px;
  2319. margin-top: 15px;
  2320. }}
  2321. .action-btn {{
  2322. flex: 1;
  2323. padding: 10px 16px;
  2324. border: none;
  2325. border-radius: 8px;
  2326. font-size: 13px;
  2327. font-weight: 600;
  2328. cursor: pointer;
  2329. transition: all 0.3s ease;
  2330. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  2331. }}
  2332. .action-btn:hover {{
  2333. transform: translateY(-2px);
  2334. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  2335. }}
  2336. .action-btn:active {{
  2337. transform: translateY(0);
  2338. }}
  2339. .btn-step1 {{
  2340. background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
  2341. color: white;
  2342. }}
  2343. .btn-step1:hover {{
  2344. background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
  2345. }}
  2346. .btn-step3 {{
  2347. background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
  2348. color: white;
  2349. }}
  2350. .btn-step3:hover {{
  2351. background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
  2352. }}
  2353. </style>
  2354. </head>
  2355. <body>
  2356. <div class="container">
  2357. <div class="header">
  2358. <h1>💡 灵感点分析可视化</h1>
  2359. <div class="header-subtitle">基于HOW人设的灵感点匹配分析结果</div>
  2360. <div class="stats-overview">
  2361. <div class="stat-box">
  2362. <div class="stat-label">灵感点总数</div>
  2363. <div class="stat-value" id="totalCount">{total_count}</div>
  2364. </div>
  2365. <div class="stat-box excellent">
  2366. <div class="stat-label">Top1优秀 (≥0.7)</div>
  2367. <div class="stat-value" id="top1ExcellentCount">{top1_excellent_count}</div>
  2368. </div>
  2369. <div class="stat-box good">
  2370. <div class="stat-label">Top1良好 (0.5-0.7)</div>
  2371. <div class="stat-value" id="top1GoodCount">{top1_good_count}</div>
  2372. </div>
  2373. <div class="stat-box normal">
  2374. <div class="stat-label">Top1一般 (0.3-0.5)</div>
  2375. <div class="stat-value" id="top1NormalCount">{top1_normal_count}</div>
  2376. </div>
  2377. <div class="stat-box need-opt">
  2378. <div class="stat-label">Top1待优化 (<0.3)</div>
  2379. <div class="stat-value" id="top1NeedOptCount">{top1_need_opt_count}</div>
  2380. </div>
  2381. <div class="stat-box">
  2382. <div class="stat-label">Top1平均分</div>
  2383. <div class="stat-value" id="avgTop1Score">{avg_top1_score:.3f}</div>
  2384. </div>
  2385. </div>
  2386. </div>
  2387. <div class="tabs-nav">
  2388. <button class="tab-button active" onclick="switchTab(event, 'tab-inspirations')">
  2389. 灵感点分析
  2390. </button>
  2391. <button class="tab-button" onclick="switchTab(event, 'tab-persona')">
  2392. 人设结构
  2393. </button>
  2394. </div>
  2395. <div id="tab-inspirations" class="tab-content active">
  2396. <div class="controls-section">
  2397. <div class="search-box">
  2398. <input type="text"
  2399. id="searchInput"
  2400. class="search-input"
  2401. placeholder="🔍 搜索灵感点名称..."
  2402. oninput="filterInspirations()">
  2403. </div>
  2404. <div class="sort-box">
  2405. <span class="sort-label">排序方式:</span>
  2406. <select id="sortSelect" class="sort-select" onchange="filterInspirations()">
  2407. <option value="score-desc">Top1分数从高到低</option>
  2408. <option value="score-asc">Top1分数从低到高</option>
  2409. <option value="name-asc">名称A-Z</option>
  2410. <option value="name-desc">名称Z-A</option>
  2411. </select>
  2412. </div>
  2413. </div>
  2414. <div class="inspirations-section">
  2415. <div class="inspirations-grid">
  2416. {cards_html_str}
  2417. </div>
  2418. </div>
  2419. </div>
  2420. <div id="tab-persona" class="tab-content">
  2421. <div class="persona-structure-section">
  2422. <h2>📚 人设结构</h2>
  2423. {persona_structure_html}
  2424. </div>
  2425. </div>
  2426. <div class="timestamp">生成时间: {timestamp}</div>
  2427. <!-- Modal -->
  2428. <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
  2429. <div class="modal-content">
  2430. <button class="modal-close" onclick="closeModal()">&times;</button>
  2431. <div class="modal-body" id="modalBody">
  2432. <!-- Content will be inserted here -->
  2433. </div>
  2434. </div>
  2435. </div>
  2436. </div>
  2437. <script>
  2438. {detail_modal_js}
  2439. </script>
  2440. </body>
  2441. </html>'''
  2442. # 写入文件
  2443. output_file = Path(output_path)
  2444. output_file.parent.mkdir(parents=True, exist_ok=True)
  2445. with open(output_file, 'w', encoding='utf-8') as f:
  2446. f.write(html_content)
  2447. return str(output_file.absolute())
  2448. def load_persona_data(persona_path: str) -> Dict[str, Any]:
  2449. """
  2450. 加载人设数据
  2451. Args:
  2452. persona_path: 人设JSON文件路径
  2453. Returns:
  2454. 人设数据字典
  2455. """
  2456. try:
  2457. with open(persona_path, 'r', encoding='utf-8') as f:
  2458. return json.load(f)
  2459. except Exception as e:
  2460. print(f"警告: 读取人设文件失败: {e}")
  2461. return {}
  2462. def main():
  2463. """主函数"""
  2464. import sys
  2465. # 配置路径
  2466. inspiration_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点"
  2467. posts_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/作者历史帖子"
  2468. persona_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/人设.json"
  2469. output_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点可视化.html"
  2470. print("=" * 60)
  2471. print("灵感点分析可视化脚本")
  2472. print("=" * 60)
  2473. # 加载数据
  2474. print("\n📂 正在加载灵感点数据...")
  2475. inspirations_data = load_inspiration_points_data(inspiration_dir)
  2476. print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
  2477. print("\n📂 正在加载帖子数据...")
  2478. posts_map = load_posts_data(posts_dir)
  2479. print(f"✅ 成功加载 {len(posts_map)} 个帖子")
  2480. print("\n📂 正在加载人设数据...")
  2481. persona_data = load_persona_data(persona_path)
  2482. print(f"✅ 成功加载人设数据")
  2483. # 生成HTML
  2484. print("\n🎨 正在生成可视化HTML...")
  2485. result_path = generate_html(inspirations_data, posts_map, persona_data, output_path)
  2486. print(f"\n✅ 可视化文件已生成!")
  2487. print(f"📄 文件路径: {result_path}")
  2488. print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
  2489. print("=" * 60)
  2490. if __name__ == "__main__":
  2491. main()