| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970 |
- """
- 灵感点分析结果可视化脚本
- 读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面
- """
- import json
- from pathlib import Path
- from typing import Dict, Any, List, Optional
- from datetime import datetime
- import html as html_module
- def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]:
- """
- 加载所有灵感点的分析结果(包含 step1 和 step3)
- Args:
- inspiration_dir: 灵感点目录路径
- Returns:
- 灵感点分析结果列表
- """
- inspiration_path = Path(inspiration_dir)
- results = []
- # 遍历所有子目录
- for subdir in inspiration_path.iterdir():
- if subdir.is_dir():
- # 查找 step1 文件
- step1_files = list(subdir.glob("all_step1_*.json"))
- # 查找 step3 文件
- step3_files = list(subdir.glob("all_step3_*.json"))
- if step1_files:
- try:
- # 读取 step1
- with open(step1_files[0], 'r', encoding='utf-8') as f:
- step1_data = json.load(f)
- # 尝试读取 step3
- step3_data = None
- if step3_files:
- try:
- with open(step3_files[0], 'r', encoding='utf-8') as f:
- step3_data = json.load(f)
- except Exception as e:
- print(f"警告: 读取 {step3_files[0]} 失败: {e}")
- results.append({
- "step1": step1_data,
- "step3": step3_data,
- "inspiration_name": subdir.name
- })
- except Exception as e:
- print(f"警告: 读取 {step1_files[0]} 失败: {e}")
- return results
- def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]:
- """
- 加载所有帖子详情数据
- Args:
- posts_dir: 帖子目录路径
- Returns:
- 帖子ID到帖子详情的映射
- """
- posts_path = Path(posts_dir)
- posts_map = {}
- for post_file in posts_path.glob("*.json"):
- try:
- with open(post_file, 'r', encoding='utf-8') as f:
- post_data = json.load(f)
- post_id = post_data.get("channel_content_id")
- if post_id:
- posts_map[post_id] = post_data
- except Exception as e:
- print(f"警告: 读取 {post_file} 失败: {e}")
- return posts_map
- def generate_inspiration_card_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成单个灵感点的卡片HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- HTML字符串
- """
- step1 = inspiration_data.get("step1", {})
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- # 从step1中获取top1匹配分数
- step1_matches = step1.get("匹配结果列表", []) if step1 else []
- step1_score = 0
- step1_match_element = ""
- if step1_matches:
- top_match = step1_matches[0]
- match_result = top_match.get("匹配结果", {})
- step1_score = match_result.get("score", 0)
- input_info = top_match.get("输入信息", {})
- step1_match_element = input_info.get("A_名称", "")
- # 确定卡片颜色(基于Step1分数)
- if step1_score >= 0.7:
- border_color = "#10b981"
- step1_color = "#10b981"
- elif step1_score >= 0.5:
- border_color = "#f59e0b"
- step1_color = "#f59e0b"
- elif step1_score >= 0.3:
- border_color = "#3b82f6"
- step1_color = "#3b82f6"
- else:
- border_color = "#ef4444"
- step1_color = "#ef4444"
- # 转义HTML
- inspiration_name_escaped = html_module.escape(inspiration_name)
- step1_match_element_escaped = html_module.escape(step1_match_element)
- # 获取Step1匹配结果(简要展示Top3)
- step1_match_preview = ""
- if step1_matches:
- preview_items = []
- for idx, match in enumerate(step1_matches[:3]):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- element_name = input_info.get("A_名称", "")
- match_score = match_result.get("score", 0)
- # 确定排名标记
- rank_emoji = ["🥇", "🥈", "🥉"][idx]
- preview_items.append(f'''
- <div class="preview-match-item">
- <span class="preview-rank">{rank_emoji}</span>
- <span class="preview-name">{html_module.escape(element_name)}</span>
- <span class="preview-score" style="color: {step1_color};">{match_score:.2f}</span>
- </div>
- ''')
- step1_match_preview = f'''
- <div class="match-preview">
- <div class="match-preview-header">🎯 Top3 匹配要素</div>
- <div class="match-preview-list">
- {"".join(preview_items)}
- </div>
- </div>
- '''
- # 获取 Step3 生成的灵感点(简要展示)
- step3_preview = ""
- step3 = inspiration_data.get("step3")
- if step3:
- step3_inspirations = step3.get("灵感点列表", [])
- if step3_inspirations:
- preview_items = []
- for idx, item in enumerate(step3_inspirations[:3]):
- path = item.get("推理路径", "")
- insp_point = item.get("灵感点", "")
- preview_items.append(f'''
- <div class="step3-preview-item">
- <span class="step3-point">💡 {html_module.escape(insp_point)}</span>
- <span class="step3-path">{html_module.escape(path)}</span>
- </div>
- ''')
- step3_preview = f'''
- <div class="step3-preview">
- <div class="step3-preview-header">✨ Step3 生成的灵感点 (前3个,共{len(step3_inspirations)}个)</div>
- <div class="step3-preview-list">
- {"".join(preview_items)}
- </div>
- </div>
- '''
- # 准备详细数据用于弹窗
- detail_data_json = json.dumps(inspiration_data, ensure_ascii=False)
- detail_data_json_escaped = html_module.escape(detail_data_json)
- # 生成详细HTML并进行HTML转义(保留兼容性)
- detail_html = generate_detail_html(inspiration_data)
- detail_html_escaped = html_module.escape(detail_html)
- # 生成 step1 和 step3 独立详情
- step1_detail_html = generate_step1_detail_html(inspiration_data)
- step1_detail_html_escaped = html_module.escape(step1_detail_html)
- step3_detail_html = generate_step3_detail_html(inspiration_data)
- step3_detail_html_escaped = html_module.escape(step3_detail_html)
- html = f'''
- <div class="inspiration-card" style="border-left-color: {border_color};"
- data-inspiration-name="{inspiration_name_escaped}"
- data-detail="{detail_data_json_escaped}"
- data-detail-html="{detail_html_escaped}"
- data-step1-detail-html="{step1_detail_html_escaped}"
- data-step3-detail-html="{step3_detail_html_escaped}"
- data-step1-score="{step1_score}">
- <div class="card-header">
- <h3 class="inspiration-name">💡 {inspiration_name_escaped}</h3>
- </div>
- <div class="score-section">
- <div class="score-item">
- <div class="score-label">Top1 分数</div>
- <div class="score-value" style="color: {step1_color};">{step1_score:.3f}</div>
- </div>
- <div class="score-item">
- <div class="score-label">🎯 匹配要素</div>
- <div class="score-match-element">{step1_match_element_escaped}</div>
- </div>
- </div>
- {step1_match_preview}
- {step3_preview}
- <div class="card-actions">
- <button class="action-btn btn-step1" onclick="event.stopPropagation(); showStep1Detail(this.closest('.inspiration-card'))">
- 🎯 查看匹配详情
- </button>
- {'<button class="action-btn btn-step3" onclick="event.stopPropagation(); showStep3Detail(this.closest' + "('.inspiration-card')" + ')">✨ 查看生成灵感</button>' if step3 else ''}
- </div>
- </div>
- '''
- return html
- def generate_step1_detail_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成 step1 匹配详情的HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- step1 详细信息的HTML字符串
- """
- import html as html_module
- step1 = inspiration_data.get("step1", {})
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- content = f'''
- <div class="modal-header">
- <h2 class="modal-title">🎯 匹配详情: {html_module.escape(inspiration_name)}</h2>
- </div>
- '''
- # 获取元数据
- metadata = step1.get("元数据", {})
- # Step1 详细信息
- if step1 and step1.get("灵感"):
- inspiration = step1.get("灵感", "")
- matches = step1.get("匹配结果列表", [])
- content += f'''
- <div class="modal-section">
- <h3>🎯 灵感人设匹配分析</h3>
- <div class="step-content">
- <div class="step-field">
- <span class="step-field-label">💡 灵感点内容:</span>
- <span class="step-field-value">{html_module.escape(inspiration)}</span>
- </div>
- '''
- # 显示匹配结果(显示Top5)
- if matches:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">匹配结果 (Top {min(len(matches), 5)}):</span>
- <div class="matches-list">
- '''
- for index, match in enumerate(matches[:5]):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- business_info = match.get("业务信息", {})
- element_name = input_info.get("A_名称", "")
- element_def = input_info.get("A_定义", "")
- context_a = input_info.get("A_Context", "")
- score = match_result.get("score", 0)
- score_explain = match_result.get("score说明", "")
- # 获取匹配关系
- match_relations = match_result.get("匹配关系", [])
- # 添加排名样式类
- rank_class = ""
- if index == 0:
- rank_class = "top1"
- elif index == 1:
- rank_class = "top2"
- elif index == 2:
- rank_class = "top3"
- content += f'''
- <div class="match-item {rank_class}">
- <div class="match-header">
- <span class="match-rank">#{index + 1}</span>
- <span class="match-element-name">🎯 人设要素: {html_module.escape(element_name)}</span>
- <span class="match-score">{score:.2f}</span>
- </div>
- '''
- if element_def:
- content += f'<div class="match-context"><strong>📖 定义:</strong> {html_module.escape(element_def)}</div>'
- if context_a:
- content += f'<div class="match-context"><strong>📍 所属:</strong> {html_module.escape(context_a)}</div>'
- # 显示B语义分析、匹配关系、A语义分析(三列布局)
- b_semantic = match_result.get("B语义分析", {})
- a_semantic = match_result.get("A语义分析", {})
- if b_semantic or a_semantic or match_relations:
- content += '<div class="semantic-analysis-with-relations">'
- # B语义分析
- if b_semantic:
- content += '''
- <div class="semantic-section b-semantic">
- <div class="semantic-header">💡 灵感点语义分析 (B)</div>
- <div class="semantic-content">
- '''
- b_substance = b_semantic.get("实质", {})
- b_form = b_semantic.get("形式", {})
- if b_substance:
- content += '<div class="semantic-part"><strong>实质:</strong></div>'
- for key, value in b_substance.items():
- content += f'''
- <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- if b_form:
- content += '<div class="semantic-part"><strong>形式:</strong></div>'
- for key, value in b_form.items():
- content += f'''
- <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 显示匹配关系(中间)
- if match_relations:
- content += '''
- <div class="match-relations-middle">
- <div class="relations-header">🔗 匹配关系</div>
- <div class="relations-content">
- '''
- for idx, rel in enumerate(match_relations):
- b_sem = rel.get("B语义", "")
- a_sem = rel.get("A语义", "")
- relation = rel.get("关系", "")
- explanation = rel.get("说明", "")
- distance_score = rel.get("距离分数", None)
- # 构建中间关系文本
- relation_middle = f'{html_module.escape(relation)}'
- if distance_score is not None:
- relation_middle += f' (分数: {distance_score:.2f})'
- # 为每个关系项生成唯一ID,用于连接线定位
- rel_id = f'rel_{index}_{idx}'
- b_sem_id = f'b_sem_{index}_{idx}'
- a_sem_id = f'a_sem_{index}_{idx}'
- content += f'''
- <div class="relation-item" id="{rel_id}">
- <div class="relation-badge">{relation_middle}</div>
- <div class="relation-semantics-ref">
- <div class="semantic-ref b-ref" data-semantic="{html_module.escape(b_sem)}">B: {html_module.escape(b_sem)}</div>
- <div class="relation-arrow">→</div>
- <div class="semantic-ref a-ref" data-semantic="{html_module.escape(a_sem)}">A: {html_module.escape(a_sem)}</div>
- </div>
- <div class="relation-explain">
- {html_module.escape(explanation)}
- </div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # A语义分析
- if a_semantic:
- content += '''
- <div class="semantic-section a-semantic">
- <div class="semantic-header">🎯 人设要素语义分析 (A)</div>
- <div class="semantic-content">
- '''
- a_substance = a_semantic.get("实质", {})
- a_form = a_semantic.get("形式", {})
- if a_substance:
- content += '<div class="semantic-part"><strong>实质:</strong></div>'
- for key, value in a_substance.items():
- content += f'''
- <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- if a_form:
- content += '<div class="semantic-part"><strong>形式:</strong></div>'
- for key, value in a_form.items():
- content += f'''
- <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- content += '</div>' # end semantic-analysis-with-relations
- # 显示分数详情(放在最后)
- rule_score = match_result.get("规则分数", 0)
- rule_score_explain = match_result.get("规则分数说明", "")
- relevance_score = match_result.get("相关性分数", 0)
- relevance_explain = match_result.get("相关性说明", "")
- score_explain = match_result.get("score说明", "")
- if rule_score or relevance_score or score_explain:
- content += '<div class="score-calculation-section">'
- if rule_score or relevance_score:
- content += '<div class="score-details">'
- if rule_score:
- content += f'''
- <div class="score-detail-item rule-score">
- <div class="score-detail-header">
- <span class="score-detail-label">📐 规则分数</span>
- <span class="score-detail-value">{rule_score:.2f}</span>
- </div>
- {f'<div class="score-detail-explain">{html_module.escape(rule_score_explain)}</div>' if rule_score_explain else ''}
- </div>
- '''
- if relevance_score:
- content += f'''
- <div class="score-detail-item relevance-score">
- <div class="score-detail-header">
- <span class="score-detail-label">🎯 相关性分数</span>
- <span class="score-detail-value">{relevance_score:.2f}</span>
- </div>
- {f'<div class="score-detail-explain">{html_module.escape(relevance_explain)}</div>' if relevance_explain else ''}
- </div>
- '''
- content += '</div>'
- if score_explain:
- content += f'<div class="match-explain final-score"><strong>🎲 最终分数计算:</strong> {html_module.escape(score_explain)}</div>'
- content += '</div>'
- content += '''
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 日志链接
- if metadata.get("log_url"):
- content += f'''
- <div class="modal-link">
- <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
- 🔗 查看详细日志
- </a>
- </div>
- '''
- return content
- def generate_step3_detail_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成 step3 生成灵感的详情HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- step3 详细信息的HTML字符串
- """
- import html as html_module
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- step3 = inspiration_data.get("step3")
- content = f'''
- <div class="modal-header">
- <h2 class="modal-title">✨ 生成灵感: {html_module.escape(inspiration_name)}</h2>
- </div>
- '''
- if not step3:
- content += '''
- <div class="empty-state">暂无生成的灵感点数据</div>
- '''
- return content
- # 获取元数据
- metadata = step3.get("元数据", {})
- anchor_info = step3.get("锚点信息", {})
- step3_inspirations = step3.get("灵感点列表", [])
- anchor_category = anchor_info.get("锚点分类", "")
- category_def = anchor_info.get("分类定义", "")
- category_context = anchor_info.get("分类上下文", "")
- content += f'''
- <div class="modal-section">
- <h3>🎯 锚点信息</h3>
- <div class="step-content">
- <div class="step-field">
- <span class="step-field-label">🎯 锚点分类:</span>
- <span class="step-field-value">{html_module.escape(anchor_category)}</span>
- </div>
- '''
- if category_def:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">📖 分类定义:</span>
- <span class="step-field-value">{html_module.escape(category_def)}</span>
- </div>
- '''
- if category_context:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">📍 分类上下文:</span>
- <span class="step-field-value">{html_module.escape(category_context)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 生成的灵感点列表
- if step3_inspirations:
- content += f'''
- <div class="modal-section">
- <h3>✨ 生成的灵感点 (共 {len(step3_inspirations)} 个)</h3>
- <div class="step-content">
- <div class="step3-inspirations-list">
- '''
- for idx, item in enumerate(step3_inspirations):
- path = item.get("推理路径", "")
- insp_point = item.get("灵感点", "")
- description = item.get("描述", "")
- content += f'''
- <div class="step3-inspiration-item">
- <div class="step3-header">
- <span class="step3-rank">#{idx + 1}</span>
- <span class="step3-point">{html_module.escape(insp_point)}</span>
- </div>
- <div class="step3-path"><strong>推理路径:</strong> {html_module.escape(path)}</div>
- <div class="step3-desc"><strong>描述:</strong> {html_module.escape(description)}</div>
- </div>
- '''
- content += '''
- </div>
- </div>
- </div>
- '''
- # 日志链接
- if metadata.get("log_url"):
- content += f'''
- <div class="modal-link">
- <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
- 🔗 查看详细日志
- </a>
- </div>
- '''
- return content
- def generate_detail_html(inspiration_data: Dict[str, Any]) -> str:
- """
- 生成灵感点的详细信息HTML
- Args:
- inspiration_data: 灵感点数据
- Returns:
- 详细信息的HTML字符串
- """
- import html as html_module
- step1 = inspiration_data.get("step1", {})
- inspiration_name = inspiration_data.get("inspiration_name", "未知灵感")
- content = f'''
- <div class="modal-header">
- <h2 class="modal-title">💡 灵感点: {html_module.escape(inspiration_name)}</h2>
- </div>
- '''
- # 获取元数据,用于后面的日志链接
- metadata = step1.get("元数据", {})
- # Step1 详细信息
- if step1 and step1.get("灵感"):
- inspiration = step1.get("灵感", "")
- matches = step1.get("匹配结果列表", [])
- content += f'''
- <div class="modal-section">
- <h3>🎯 灵感人设匹配分析</h3>
- <div class="step-content">
- <div class="step-field">
- <span class="step-field-label">💡 灵感点内容:</span>
- <span class="step-field-value">{html_module.escape(inspiration)}</span>
- </div>
- '''
- # 显示匹配结果(显示Top5)
- if matches:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">匹配结果 (Top {min(len(matches), 5)}):</span>
- <div class="matches-list">
- '''
- for index, match in enumerate(matches[:5]):
- input_info = match.get("输入信息", {})
- match_result = match.get("匹配结果", {})
- business_info = match.get("业务信息", {})
- element_name = input_info.get("A_名称", "")
- element_def = input_info.get("A_定义", "")
- context_a = input_info.get("A_Context", "")
- score = match_result.get("score", 0)
- score_explain = match_result.get("score说明", "")
- # 获取匹配关系
- match_relations = match_result.get("匹配关系", [])
- # 添加排名样式类
- rank_class = ""
- if index == 0:
- rank_class = "top1"
- elif index == 1:
- rank_class = "top2"
- elif index == 2:
- rank_class = "top3"
- content += f'''
- <div class="match-item {rank_class}">
- <div class="match-header">
- <span class="match-rank">#{index + 1}</span>
- <span class="match-element-name">🎯 人设要素: {html_module.escape(element_name)}</span>
- <span class="match-score">{score:.2f}</span>
- </div>
- '''
- if element_def:
- content += f'<div class="match-context"><strong>📖 定义:</strong> {html_module.escape(element_def)}</div>'
- if context_a:
- content += f'<div class="match-context"><strong>📍 所属:</strong> {html_module.escape(context_a)}</div>'
- # 显示B语义分析、匹配关系、A语义分析(三列布局)
- b_semantic = match_result.get("B语义分析", {})
- a_semantic = match_result.get("A语义分析", {})
- if b_semantic or a_semantic or match_relations:
- content += '<div class="semantic-analysis-with-relations">'
- # B语义分析
- if b_semantic:
- content += '''
- <div class="semantic-section b-semantic">
- <div class="semantic-header">💡 灵感点语义分析 (B)</div>
- <div class="semantic-content">
- '''
- b_substance = b_semantic.get("实质", {})
- b_form = b_semantic.get("形式", {})
- if b_substance:
- content += '<div class="semantic-part"><strong>实质:</strong></div>'
- for key, value in b_substance.items():
- content += f'''
- <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- if b_form:
- content += '<div class="semantic-part"><strong>形式:</strong></div>'
- for key, value in b_form.items():
- content += f'''
- <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 显示匹配关系(中间)
- if match_relations:
- content += '''
- <div class="match-relations-middle">
- <div class="relations-header">🔗 匹配关系</div>
- <div class="relations-content">
- '''
- for idx, rel in enumerate(match_relations):
- b_sem = rel.get("B语义", "")
- a_sem = rel.get("A语义", "")
- relation = rel.get("关系", "")
- explanation = rel.get("说明", "")
- distance_score = rel.get("距离分数", None)
- # 构建中间关系文本
- relation_middle = f'{html_module.escape(relation)}'
- if distance_score is not None:
- relation_middle += f' (分数: {distance_score:.2f})'
- # 为每个关系项生成唯一ID,用于连接线定位
- rel_id = f'rel_{index}_{idx}'
- b_sem_id = f'b_sem_{index}_{idx}'
- a_sem_id = f'a_sem_{index}_{idx}'
- content += f'''
- <div class="relation-item" id="{rel_id}">
- <div class="relation-badge">{relation_middle}</div>
- <div class="relation-semantics-ref">
- <div class="semantic-ref b-ref" data-semantic="{html_module.escape(b_sem)}">B: {html_module.escape(b_sem)}</div>
- <div class="relation-arrow">→</div>
- <div class="semantic-ref a-ref" data-semantic="{html_module.escape(a_sem)}">A: {html_module.escape(a_sem)}</div>
- </div>
- <div class="relation-explain">
- {html_module.escape(explanation)}
- </div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # A语义分析
- if a_semantic:
- content += '''
- <div class="semantic-section a-semantic">
- <div class="semantic-header">🎯 人设要素语义分析 (A)</div>
- <div class="semantic-content">
- '''
- a_substance = a_semantic.get("实质", {})
- a_form = a_semantic.get("形式", {})
- if a_substance:
- content += '<div class="semantic-part"><strong>实质:</strong></div>'
- for key, value in a_substance.items():
- content += f'''
- <div class="semantic-item substance" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- if a_form:
- content += '<div class="semantic-part"><strong>形式:</strong></div>'
- for key, value in a_form.items():
- content += f'''
- <div class="semantic-item form" data-semantic-key="{html_module.escape(key)}">
- <span class="semantic-key">{html_module.escape(key)}:</span>
- <span class="semantic-value">{html_module.escape(value)}</span>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- content += '</div>' # end semantic-analysis-with-relations
- # 显示分数详情(放在最后)
- rule_score = match_result.get("规则分数", 0)
- rule_score_explain = match_result.get("规则分数说明", "")
- relevance_score = match_result.get("相关性分数", 0)
- relevance_explain = match_result.get("相关性说明", "")
- score_explain = match_result.get("score说明", "")
- if rule_score or relevance_score or score_explain:
- content += '<div class="score-calculation-section">'
- if rule_score or relevance_score:
- content += '<div class="score-details">'
- if rule_score:
- content += f'''
- <div class="score-detail-item rule-score">
- <div class="score-detail-header">
- <span class="score-detail-label">📐 规则分数</span>
- <span class="score-detail-value">{rule_score:.2f}</span>
- </div>
- {f'<div class="score-detail-explain">{html_module.escape(rule_score_explain)}</div>' if rule_score_explain else ''}
- </div>
- '''
- if relevance_score:
- content += f'''
- <div class="score-detail-item relevance-score">
- <div class="score-detail-header">
- <span class="score-detail-label">🎯 相关性分数</span>
- <span class="score-detail-value">{relevance_score:.2f}</span>
- </div>
- {f'<div class="score-detail-explain">{html_module.escape(relevance_explain)}</div>' if relevance_explain else ''}
- </div>
- '''
- content += '</div>'
- if score_explain:
- content += f'<div class="match-explain final-score"><strong>🎲 最终分数计算:</strong> {html_module.escape(score_explain)}</div>'
- content += '</div>'
- content += '''
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # Step3 详细信息
- step3 = inspiration_data.get("step3")
- if step3:
- anchor_info = step3.get("锚点信息", {})
- step3_inspirations = step3.get("灵感点列表", [])
- anchor_category = anchor_info.get("锚点分类", "")
- category_def = anchor_info.get("分类定义", "")
- content += f'''
- <div class="modal-section">
- <h3>✨ Step3: 生成的灵感点</h3>
- <div class="step-content">
- <div class="step-field">
- <span class="step-field-label">🎯 锚点分类:</span>
- <span class="step-field-value">{html_module.escape(anchor_category)}</span>
- </div>
- '''
- if category_def:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">📖 分类定义:</span>
- <span class="step-field-value">{html_module.escape(category_def)}</span>
- </div>
- '''
- if step3_inspirations:
- content += f'''
- <div class="step-field">
- <span class="step-field-label">生成的灵感点列表 (共 {len(step3_inspirations)} 个):</span>
- <div class="step3-inspirations-list">
- '''
- for idx, item in enumerate(step3_inspirations):
- path = item.get("推理路径", "")
- insp_point = item.get("灵感点", "")
- description = item.get("描述", "")
- content += f'''
- <div class="step3-inspiration-item">
- <div class="step3-header">
- <span class="step3-rank">#{idx + 1}</span>
- <span class="step3-point">{html_module.escape(insp_point)}</span>
- </div>
- <div class="step3-path"><strong>推理路径:</strong> {html_module.escape(path)}</div>
- <div class="step3-desc"><strong>描述:</strong> {html_module.escape(description)}</div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- content += '''
- </div>
- </div>
- '''
- # 日志链接
- if metadata.get("log_url"):
- content += f'''
- <div class="modal-link">
- <a href="{metadata["log_url"]}" target="_blank" class="modal-link-btn">
- 🔗 查看详细日志
- </a>
- </div>
- '''
- return content
- def generate_detail_modal_content_js() -> str:
- """
- 生成详情弹窗内容的JavaScript函数
- Returns:
- JavaScript代码字符串
- """
- return '''
- // Tab切换功能
- function switchTab(event, tabId) {
- // 移除所有tab的active状态
- const tabButtons = document.querySelectorAll('.tab-button');
- tabButtons.forEach(button => {
- button.classList.remove('active');
- });
- // 隐藏所有tab内容
- const tabContents = document.querySelectorAll('.tab-content');
- tabContents.forEach(content => {
- content.classList.remove('active');
- });
- // 激活当前tab
- event.currentTarget.classList.add('active');
- document.getElementById(tabId).classList.add('active');
- }
- function showInspirationDetail(element) {
- const detailHtml = element.dataset.detailHtml;
- const modal = document.getElementById('detailModal');
- const modalBody = document.getElementById('modalBody');
- modalBody.innerHTML = detailHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- }
- function showStep1Detail(element) {
- const step1DetailHtml = element.dataset.step1DetailHtml;
- const modal = document.getElementById('detailModal');
- const modalBody = document.getElementById('modalBody');
- modalBody.innerHTML = step1DetailHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- }
- function showStep3Detail(element) {
- const step3DetailHtml = element.dataset.step3DetailHtml;
- const modal = document.getElementById('detailModal');
- const modalBody = document.getElementById('modalBody');
- modalBody.innerHTML = step3DetailHtml;
- modal.classList.add('active');
- document.body.style.overflow = 'hidden';
- }
- function closeModal() {
- const modal = document.getElementById('detailModal');
- modal.classList.remove('active');
- document.body.style.overflow = '';
- }
- function closeModalOnOverlay(event) {
- if (event.target.id === 'detailModal') {
- closeModal();
- }
- }
- // ESC键关闭Modal
- document.addEventListener('keydown', function(event) {
- if (event.key === 'Escape') {
- closeModal();
- }
- });
- // 搜索和过滤功能
- function filterInspirations() {
- const searchInput = document.getElementById('searchInput').value.toLowerCase();
- const sortSelect = document.getElementById('sortSelect').value;
- const cards = document.querySelectorAll('.inspiration-card');
- let visibleCards = Array.from(cards);
- // 搜索过滤
- visibleCards.forEach(card => {
- const name = card.dataset.inspirationName.toLowerCase();
- if (name.includes(searchInput)) {
- card.style.display = '';
- } else {
- card.style.display = 'none';
- }
- });
- // 获取可见的卡片
- visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
- // 排序
- if (sortSelect === 'score-desc' || sortSelect === 'score-asc') {
- visibleCards.sort((a, b) => {
- const scoreA = parseFloat(a.dataset.step1Score) || 0;
- const scoreB = parseFloat(b.dataset.step1Score) || 0;
- return sortSelect === 'score-desc' ? scoreB - scoreA : scoreA - scoreB;
- });
- } else if (sortSelect === 'name-asc' || sortSelect === 'name-desc') {
- visibleCards.sort((a, b) => {
- const nameA = a.dataset.inspirationName;
- const nameB = b.dataset.inspirationName;
- return sortSelect === 'name-asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
- });
- }
- // 重新排列卡片
- const container = document.querySelector('.inspirations-grid');
- visibleCards.forEach(card => {
- container.appendChild(card);
- });
- // 更新统计
- updateStats();
- }
- function updateStats() {
- const cards = document.querySelectorAll('.inspiration-card');
- const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
- document.getElementById('totalCount').textContent = visibleCards.length;
- let excellentCount = 0;
- let goodCount = 0;
- let normalCount = 0;
- let needOptCount = 0;
- let totalScore = 0;
- visibleCards.forEach(card => {
- const score = parseFloat(card.dataset.step1Score) || 0;
- totalScore += score;
- // 分数统计
- if (score >= 0.7) excellentCount++;
- else if (score >= 0.5) goodCount++;
- else if (score >= 0.3) normalCount++;
- else needOptCount++;
- });
- document.getElementById('top1ExcellentCount').textContent = excellentCount;
- document.getElementById('top1GoodCount').textContent = goodCount;
- document.getElementById('top1NormalCount').textContent = normalCount;
- document.getElementById('top1NeedOptCount').textContent = needOptCount;
- const avgScore = visibleCards.length > 0 ? (totalScore / visibleCards.length).toFixed(3) : '0.000';
- document.getElementById('avgTop1Score').textContent = avgScore;
- }
- '''
- def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str:
- """
- 生成人设结构的树状HTML
- Args:
- persona_data: 人设数据
- Returns:
- 人设结构的HTML字符串
- """
- if not persona_data:
- return '<div class="empty-state">暂无人设数据</div>'
- inspiration_list = persona_data.get("灵感点列表", [])
- if not inspiration_list:
- return '<div class="empty-state">暂无灵感点列表数据</div>'
- html_parts = ['<div class="tree">']
- for perspective_idx, perspective in enumerate(inspiration_list):
- perspective_name = perspective.get("视角名称", "未知视角")
- perspective_desc = perspective.get("视角描述", "")
- pattern_list = perspective.get("模式列表", [])
- # 一级节点:视角
- html_parts.append(f'''
- <ul>
- <li>
- <div class="tree-node level-1">
- <span class="node-icon">📁</span>
- <span class="node-name">{html_module.escape(perspective_name)}</span>
- <span class="node-count">{len(pattern_list)}个分类</span>
- </div>
- ''')
- if perspective_desc:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(perspective_desc)}</div>
- ''')
- # 二级节点:分类
- if pattern_list:
- html_parts.append('<ul>')
- for pattern in pattern_list:
- category_name = pattern.get("分类名称", "未知分类")
- core_definition = pattern.get("核心定义", "")
- subcategories = pattern.get("二级细分", [])
- total_posts = sum(len(sub.get("帖子ID列表", [])) for sub in subcategories)
- html_parts.append(f'''
- <li>
- <div class="tree-node level-2">
- <span class="node-icon">📂</span>
- <span class="node-name">{html_module.escape(category_name)}</span>
- <span class="node-count">{total_posts}个帖子</span>
- </div>
- ''')
- if core_definition:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(core_definition)}</div>
- ''')
- # 三级节点:细分
- if subcategories:
- html_parts.append('<ul>')
- for subcategory in subcategories:
- sub_name = subcategory.get("分类名称", "未知细分")
- sub_definition = subcategory.get("分类定义", "")
- post_ids = subcategory.get("帖子ID列表", [])
- html_parts.append(f'''
- <li>
- <div class="tree-node level-3">
- <span class="node-icon">📄</span>
- <span class="node-name">{html_module.escape(sub_name)}</span>
- <span class="node-count">{len(post_ids)}个帖子</span>
- </div>
- ''')
- if sub_definition:
- html_parts.append(f'''
- <div class="node-desc">{html_module.escape(sub_definition)}</div>
- ''')
- if post_ids:
- html_parts.append(f'''
- <div class="node-posts">
- <span class="posts-label">📋 帖子ID:</span>
- <span class="posts-ids">{", ".join([html_module.escape(str(pid)) for pid in post_ids[:5]])}</span>
- {f'<span class="posts-more">... 等{len(post_ids)}个</span>' if len(post_ids) > 5 else ''}
- </div>
- ''')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</li>')
- html_parts.append('</ul>')
- html_parts.append('</div>')
- return ''.join(html_parts)
- def generate_html(
- inspirations_data: List[Dict[str, Any]],
- posts_map: Dict[str, Dict[str, Any]],
- persona_data: Dict[str, Any],
- output_path: str
- ) -> str:
- """
- 生成完整的可视化HTML
- Args:
- inspirations_data: 灵感点数据列表
- posts_map: 帖子数据映射
- persona_data: 人设数据
- output_path: 输出文件路径
- Returns:
- 输出文件路径
- """
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- # 统计信息
- total_count = len(inspirations_data)
- # 获取所有分数用于统计
- def get_top1_score(d):
- step1 = d.get("step1", {})
- matches = step1.get("匹配结果列表", [])
- if matches:
- return matches[0].get("匹配结果", {}).get("score", 0)
- return 0
- # Top1分数统计
- top1_excellent_count = sum(1 for d in inspirations_data if get_top1_score(d) >= 0.7)
- top1_good_count = sum(1 for d in inspirations_data if 0.5 <= get_top1_score(d) < 0.7)
- top1_normal_count = sum(1 for d in inspirations_data if 0.3 <= get_top1_score(d) < 0.5)
- top1_need_opt_count = sum(1 for d in inspirations_data if get_top1_score(d) < 0.3)
- # 平均分数
- total_top1_score = sum(get_top1_score(d) for d in inspirations_data)
- avg_top1_score = total_top1_score / total_count if total_count > 0 else 0
- # 按Top1分数排序
- inspirations_data_sorted = sorted(
- inspirations_data,
- key=lambda x: get_top1_score(x),
- reverse=True
- )
- # 生成卡片HTML
- cards_html = [generate_inspiration_card_html(data) for data in inspirations_data_sorted]
- cards_html_str = '\n'.join(cards_html)
- # 生成人设结构HTML
- persona_structure_html = generate_persona_structure_html(persona_data)
- # 生成JavaScript
- detail_modal_js = generate_detail_modal_content_js()
- # 完整HTML
- html_content = f'''<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>灵感点分析可视化</title>
- <style>
- * {{
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }}
- body {{
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #333;
- line-height: 1.6;
- min-height: 100vh;
- padding: 20px;
- }}
- .container {{
- max-width: 1600px;
- margin: 0 auto;
- }}
- .header {{
- background: white;
- padding: 40px;
- border-radius: 16px;
- margin-bottom: 30px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
- }}
- .header h1 {{
- font-size: 42px;
- margin-bottom: 10px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: 800;
- }}
- .header-subtitle {{
- font-size: 16px;
- color: #6b7280;
- margin-bottom: 30px;
- }}
- .stats-overview {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: 20px;
- margin-top: 25px;
- }}
- .stat-box {{
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- padding: 20px;
- border-radius: 12px;
- text-align: center;
- transition: transform 0.3s ease;
- }}
- .stat-box:hover {{
- transform: translateY(-5px);
- }}
- .stat-label {{
- font-size: 13px;
- color: #6b7280;
- margin-bottom: 8px;
- font-weight: 600;
- }}
- .stat-value {{
- font-size: 32px;
- font-weight: 700;
- color: #1a1a1a;
- }}
- .stat-box.excellent .stat-value {{
- color: #10b981;
- }}
- .stat-box.good .stat-value {{
- color: #f59e0b;
- }}
- .stat-box.normal .stat-value {{
- color: #3b82f6;
- }}
- .stat-box.need-opt .stat-value {{
- color: #ef4444;
- }}
- .controls-section {{
- background: #f9fafb;
- padding: 25px;
- border-radius: 12px;
- margin-bottom: 30px;
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
- align-items: center;
- }}
- .search-box {{
- flex: 1;
- min-width: 250px;
- }}
- .search-input {{
- width: 100%;
- padding: 12px 20px;
- border: 2px solid #e5e7eb;
- border-radius: 10px;
- font-size: 15px;
- transition: all 0.3s;
- }}
- .search-input:focus {{
- outline: none;
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
- }}
- .sort-box {{
- display: flex;
- align-items: center;
- gap: 12px;
- }}
- .sort-label {{
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- }}
- .sort-select {{
- padding: 10px 16px;
- border: 2px solid #e5e7eb;
- border-radius: 10px;
- font-size: 14px;
- background: white;
- cursor: pointer;
- transition: all 0.3s;
- }}
- .sort-select:focus {{
- outline: none;
- border-color: #667eea;
- }}
- .inspirations-section {{
- padding: 0;
- }}
- .inspirations-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
- gap: 25px;
- }}
- .inspiration-card {{
- background: white;
- border-radius: 14px;
- padding: 25px;
- border-left: 6px solid #10b981;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
- position: relative;
- }}
- .inspiration-card:hover {{
- transform: translateY(-8px);
- box-shadow: 0 12px 30px rgba(102, 126, 234, 0.2);
- }}
- .card-header {{
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 20px;
- gap: 12px;
- }}
- .inspiration-name {{
- font-size: 19px;
- font-weight: 700;
- color: #1a1a1a;
- line-height: 1.4;
- flex: 1;
- }}
- .grade-badge {{
- background: #10b981;
- color: white;
- padding: 6px 14px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 700;
- white-space: nowrap;
- }}
- .score-section {{
- display: flex;
- align-items: center;
- gap: 25px;
- margin-bottom: 20px;
- padding: 20px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- border-radius: 12px;
- }}
- .score-item {{
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- flex: 1;
- }}
- .main-score {{
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- }}
- .score-circle {{
- width: 90px;
- height: 90px;
- border-radius: 50%;
- border: 6px solid #10b981;
- display: flex;
- align-items: center;
- justify-content: center;
- background: white;
- }}
- .score-value {{
- font-size: 26px;
- font-weight: 800;
- color: #10b981;
- }}
- .score-label {{
- font-size: 12px;
- color: #6b7280;
- font-weight: 600;
- }}
- .score-match-element {{
- font-size: 13px;
- color: #374151;
- font-weight: 600;
- text-align: center;
- padding: 4px 8px;
- background: white;
- border-radius: 6px;
- }}
- .sub-scores {{
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }}
- .sub-score-item {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 15px;
- background: white;
- border-radius: 8px;
- }}
- .sub-score-label {{
- font-size: 13px;
- color: #6b7280;
- font-weight: 600;
- }}
- .sub-score-value {{
- font-size: 18px;
- font-weight: 700;
- color: #2563eb;
- }}
- .metrics-section {{
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-bottom: 15px;
- }}
- .metric-item {{
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 13px;
- color: #4b5563;
- }}
- .metric-icon {{
- font-size: 16px;
- }}
- .metric-label {{
- font-weight: 600;
- }}
- .metric-value {{
- color: #1f2937;
- }}
- .match-preview {{
- background: #f9fafb;
- padding: 12px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 3px solid #8b5cf6;
- }}
- .match-preview-header {{
- font-size: 12px;
- font-weight: 600;
- color: #6b7280;
- margin-bottom: 6px;
- }}
- .match-preview-content {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- }}
- .match-preview-name {{
- font-size: 13px;
- color: #1f2937;
- flex: 1;
- }}
- .match-preview-score {{
- font-size: 16px;
- font-weight: 700;
- }}
- .preview-parts {{
- margin-top: 8px;
- padding: 8px 10px;
- border-radius: 6px;
- font-size: 12px;
- line-height: 1.6;
- }}
- .preview-parts.same {{
- background: #f0fdf4;
- color: #15803d;
- border-left: 3px solid #10b981;
- }}
- .preview-parts.increment {{
- background: #fff7ed;
- color: #92400e;
- border-left: 3px solid #f59e0b;
- margin-top: 6px;
- }}
- .preview-parts strong {{
- font-weight: 700;
- margin-right: 6px;
- }}
- .score-divider {{
- width: 1px;
- height: 40px;
- background: #e5e7eb;
- }}
- .click-hint {{
- position: absolute;
- bottom: 15px;
- right: 15px;
- font-size: 12px;
- color: #8b5cf6;
- font-weight: 700;
- opacity: 0;
- transition: opacity 0.3s ease;
- background: rgba(139, 92, 246, 0.1);
- padding: 6px 12px;
- border-radius: 8px;
- }}
- .inspiration-card:hover .click-hint {{
- opacity: 1;
- }}
- /* Modal样式 */
- .modal-overlay {{
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- padding: 20px;
- overflow-y: auto;
- }}
- .modal-overlay.active {{
- display: flex;
- }}
- .modal-content {{
- background: white;
- border-radius: 16px;
- max-width: 1200px;
- width: 100%;
- max-height: 90vh;
- overflow-y: auto;
- position: relative;
- }}
- .modal-close {{
- position: sticky;
- top: 0;
- right: 0;
- background: white;
- border: none;
- font-size: 32px;
- color: #6b7280;
- cursor: pointer;
- padding: 15px 20px;
- z-index: 10;
- text-align: right;
- border-bottom: 1px solid #e5e7eb;
- }}
- .modal-close:hover {{
- color: #1f2937;
- }}
- .modal-body {{
- padding: 30px;
- }}
- .modal-header {{
- margin-bottom: 25px;
- padding-bottom: 20px;
- border-bottom: 2px solid #e5e7eb;
- }}
- .modal-title {{
- font-size: 28px;
- font-weight: 800;
- color: #1a1a1a;
- }}
- .modal-section {{
- margin-bottom: 30px;
- }}
- .modal-section h3 {{
- font-size: 20px;
- font-weight: 700;
- color: #374151;
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 2px solid #f3f4f6;
- }}
- .info-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 15px;
- }}
- .info-item {{
- background: #f9fafb;
- padding: 12px 16px;
- border-radius: 8px;
- border-left: 3px solid #8b5cf6;
- }}
- .info-label {{
- font-weight: 600;
- color: #6b7280;
- font-size: 13px;
- margin-right: 8px;
- }}
- .info-value {{
- color: #1f2937;
- font-size: 14px;
- }}
- .metrics-grid {{
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 15px;
- }}
- .metric-box {{
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- padding: 20px;
- border-radius: 12px;
- text-align: center;
- color: white;
- }}
- .metric-box.wide {{
- grid-column: span 2;
- }}
- .metric-box-label {{
- font-size: 13px;
- opacity: 0.9;
- margin-bottom: 8px;
- font-weight: 600;
- }}
- .metric-box-value {{
- font-size: 28px;
- font-weight: 700;
- }}
- .metric-box-value.small {{
- font-size: 16px;
- }}
- .step-content {{
- background: #f9fafb;
- padding: 20px;
- border-radius: 12px;
- }}
- .step-field {{
- margin-bottom: 20px;
- }}
- .step-field-label {{
- font-weight: 700;
- color: #374151;
- font-size: 14px;
- margin-bottom: 8px;
- display: block;
- }}
- .step-field-value {{
- color: #1f2937;
- font-size: 15px;
- line-height: 1.7;
- }}
- .matches-list {{
- display: flex;
- flex-direction: column;
- gap: 15px;
- margin-top: 10px;
- }}
- .match-item {{
- background: white;
- padding: 18px;
- border-radius: 10px;
- border-left: 5px solid #3b82f6;
- }}
- .match-item.top1 {{
- border-left-color: #fbbf24;
- background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, white 100%);
- }}
- .match-item.top2 {{
- border-left-color: #c0c0c0;
- background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 50%, white 100%);
- }}
- .match-item.top3 {{
- border-left-color: #cd7f32;
- background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 50%, white 100%);
- }}
- .match-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- gap: 10px;
- }}
- .match-rank {{
- font-size: 18px;
- font-weight: 800;
- color: #6b7280;
- }}
- .match-element-name {{
- flex: 1;
- font-size: 16px;
- font-weight: 700;
- color: #1f2937;
- }}
- .match-score {{
- font-size: 22px;
- font-weight: 800;
- color: #2563eb;
- background: white;
- padding: 6px 14px;
- border-radius: 8px;
- }}
- .match-detail {{
- background: rgba(255, 255, 255, 0.7);
- padding: 10px;
- border-radius: 6px;
- margin-bottom: 10px;
- font-size: 13px;
- color: #4b5563;
- }}
- .match-reason {{
- color: #1f2937;
- font-size: 14px;
- line-height: 1.7;
- }}
- .increment-matches {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 10px;
- }}
- .increment-item {{
- background: white;
- padding: 15px;
- border-radius: 8px;
- border-left: 4px solid #10b981;
- }}
- .increment-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }}
- .increment-words {{
- font-weight: 700;
- color: #1f2937;
- font-size: 15px;
- }}
- .increment-score {{
- font-size: 20px;
- font-weight: 800;
- color: #10b981;
- }}
- .increment-reason {{
- color: #4b5563;
- font-size: 13px;
- line-height: 1.6;
- }}
- .empty-state {{
- text-align: center;
- padding: 40px;
- color: #9ca3af;
- font-size: 14px;
- }}
- .modal-link {{
- margin-top: 25px;
- padding-top: 20px;
- border-top: 2px solid #e5e7eb;
- text-align: center;
- }}
- .modal-link-btn {{
- display: inline-flex;
- align-items: center;
- gap: 10px;
- padding: 12px 24px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- text-decoration: none;
- border-radius: 10px;
- font-size: 15px;
- font-weight: 600;
- transition: all 0.3s;
- }}
- .modal-link-btn:hover {{
- transform: translateY(-2px);
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
- }}
- .timestamp {{
- text-align: center;
- color: white;
- font-size: 13px;
- margin-top: 30px;
- opacity: 0.8;
- }}
- .match-context {{
- background: #f3f4f6;
- padding: 8px 12px;
- border-radius: 6px;
- margin: 8px 0;
- font-size: 12px;
- color: #6b7280;
- line-height: 1.6;
- }}
- .match-explain {{
- background: #fef3c7;
- padding: 10px 12px;
- border-radius: 6px;
- margin: 10px 0;
- font-size: 13px;
- color: #92400e;
- line-height: 1.7;
- border-left: 3px solid #f59e0b;
- }}
- .match-parts {{
- margin: 12px 0;
- border-radius: 8px;
- overflow: hidden;
- }}
- .match-parts.same-parts {{
- background: #f0fdf4;
- border: 2px solid #10b981;
- }}
- .match-parts.increment-parts {{
- background: #fff7ed;
- border: 2px solid #f59e0b;
- }}
- .parts-header {{
- font-weight: 700;
- padding: 10px 12px;
- font-size: 13px;
- }}
- .same-parts .parts-header {{
- background: #dcfce7;
- color: #15803d;
- }}
- .increment-parts .parts-header {{
- background: #fed7aa;
- color: #92400e;
- }}
- .parts-content {{
- padding: 8px 12px;
- }}
- .part-item {{
- padding: 6px 0;
- border-bottom: 1px solid rgba(0,0,0,0.05);
- font-size: 13px;
- line-height: 1.6;
- }}
- .part-item:last-child {{
- border-bottom: none;
- }}
- .part-key {{
- font-weight: 600;
- color: #374151;
- margin-right: 6px;
- }}
- .part-value {{
- color: #1f2937;
- }}
- .increment-context {{
- background: #fef3c7;
- padding: 10px 12px;
- border-radius: 6px;
- margin: 10px 0;
- font-size: 12px;
- color: #92400e;
- line-height: 1.6;
- border-left: 3px solid #f59e0b;
- }}
- /* 语义分析样式 */
- .semantic-analysis {{
- margin: 15px 0;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 15px;
- }}
- /* 三列布局:B语义、匹配关系、A语义 */
- .semantic-analysis-with-relations {{
- margin: 15px 0;
- display: grid;
- grid-template-columns: 1fr auto 1fr;
- gap: 20px;
- align-items: start;
- }}
- .semantic-section {{
- border-radius: 8px;
- overflow: hidden;
- border: 2px solid;
- }}
- .semantic-section.b-semantic {{
- background: #fffbeb;
- border-color: #f59e0b;
- }}
- .semantic-section.a-semantic {{
- background: #f0fdf4;
- border-color: #10b981;
- }}
- .semantic-header {{
- font-weight: 700;
- padding: 10px 12px;
- font-size: 13px;
- }}
- .b-semantic .semantic-header {{
- background: #fef3c7;
- color: #92400e;
- }}
- .a-semantic .semantic-header {{
- background: #dcfce7;
- color: #15803d;
- }}
- .semantic-content {{
- padding: 10px 12px;
- }}
- .semantic-part {{
- font-weight: 600;
- font-size: 12px;
- color: #6b7280;
- margin: 10px 0 6px 0;
- }}
- .semantic-part:first-child {{
- margin-top: 0;
- }}
- .semantic-item {{
- padding: 8px 10px;
- margin: 6px 0;
- border-radius: 6px;
- font-size: 12px;
- line-height: 1.6;
- }}
- .semantic-item.substance {{
- background: rgba(255, 255, 255, 0.7);
- border-left: 3px solid #f59e0b;
- }}
- .semantic-item.form {{
- background: rgba(255, 255, 255, 0.5);
- border-left: 3px solid #94a3b8;
- }}
- .semantic-key {{
- font-weight: 700;
- color: #374151;
- margin-right: 6px;
- }}
- .semantic-value {{
- color: #1f2937;
- }}
- @media (max-width: 1024px) {{
- .semantic-analysis {{
- grid-template-columns: 1fr;
- }}
- .semantic-analysis-with-relations {{
- grid-template-columns: 1fr;
- }}
- }}
- /* 中间匹配关系列样式 */
- .match-relations-middle {{
- display: flex;
- flex-direction: column;
- justify-content: center;
- min-width: 180px;
- max-width: 250px;
- position: relative;
- }}
- .match-relations-middle .relations-header {{
- background: #dbeafe;
- color: #1e40af;
- font-weight: 700;
- padding: 8px 12px;
- font-size: 12px;
- text-align: center;
- border-radius: 6px;
- margin-bottom: 12px;
- }}
- .match-relations-middle .relations-content {{
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 15px 10px;
- position: relative;
- }}
- .relation-item {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- align-items: center;
- }}
- .relation-badge {{
- background: #e0f2fe;
- color: #0c4a6e;
- padding: 12px 16px;
- border-radius: 8px;
- font-weight: 700;
- font-size: 14px;
- border: 2px solid #3b82f6;
- box-shadow: 0 2px 6px rgba(0,0,0,0.15);
- text-align: center;
- white-space: nowrap;
- }}
- .relation-semantics-ref {{
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
- justify-content: center;
- flex-wrap: wrap;
- }}
- .semantic-ref {{
- padding: 6px 12px;
- border-radius: 6px;
- font-size: 11px;
- font-weight: 600;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
- }}
- .semantic-ref.b-ref {{
- background: #fef3c7;
- color: #92400e;
- border: 1px solid #f59e0b;
- }}
- .semantic-ref.a-ref {{
- background: #dcfce7;
- color: #15803d;
- border: 1px solid #10b981;
- }}
- .relation-semantics-ref .relation-arrow {{
- font-size: 16px;
- color: #ef4444;
- font-weight: 900;
- }}
- .match-relations-middle .relation-explain {{
- font-size: 12px;
- color: #374151;
- line-height: 1.6;
- padding: 12px;
- background: rgba(255, 255, 255, 0.95);
- border-radius: 6px;
- border: 1px solid #cbd5e1;
- width: 100%;
- text-align: left;
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
- }}
- /* 当鼠标悬停在语义引用上时,高亮对应的左右语义项 */
- .semantic-ref:hover {{
- box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
- transform: scale(1.05);
- cursor: pointer;
- transition: all 0.2s;
- }}
- /* 匹配关系样式(旧的,保留用于其他地方) */
- .match-relations {{
- margin: 12px 0;
- background: #f0f9ff;
- border-radius: 8px;
- border: 2px solid #3b82f6;
- overflow: hidden;
- }}
- .relations-header {{
- background: #dbeafe;
- color: #1e40af;
- font-weight: 700;
- padding: 10px 12px;
- font-size: 13px;
- }}
- .relations-content {{
- padding: 8px 12px;
- }}
- /* 分数计算区域样式 */
- .score-calculation-section {{
- margin-top: 20px;
- padding-top: 20px;
- border-top: 2px solid #e5e7eb;
- }}
- /* 分数详情样式 */
- .score-details {{
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
- margin: 15px 0;
- }}
- .score-detail-item {{
- border-radius: 8px;
- padding: 12px;
- border: 2px solid;
- }}
- .score-detail-item.rule-score {{
- background: #fef3c7;
- border-color: #f59e0b;
- }}
- .score-detail-item.relevance-score {{
- background: #e0f2fe;
- border-color: #3b82f6;
- }}
- .score-detail-header {{
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- }}
- .score-detail-label {{
- font-weight: 700;
- font-size: 13px;
- }}
- .rule-score .score-detail-label {{
- color: #92400e;
- }}
- .relevance-score .score-detail-label {{
- color: #1e40af;
- }}
- .score-detail-value {{
- font-size: 20px;
- font-weight: 800;
- }}
- .rule-score .score-detail-value {{
- color: #f59e0b;
- }}
- .relevance-score .score-detail-value {{
- color: #3b82f6;
- }}
- .score-detail-explain {{
- font-size: 12px;
- line-height: 1.6;
- color: #374151;
- }}
- @media (max-width: 768px) {{
- .score-details {{
- grid-template-columns: 1fr;
- }}
- }}
- /* 匹配预览列表样式 */
- .match-preview-list {{
- display: flex;
- flex-direction: column;
- gap: 8px;
- }}
- .preview-match-item {{
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 12px;
- background: white;
- border-radius: 6px;
- border-left: 3px solid #e5e7eb;
- }}
- .preview-rank {{
- font-size: 16px;
- font-weight: 800;
- }}
- .preview-name {{
- flex: 1;
- font-size: 13px;
- font-weight: 600;
- color: #374151;
- }}
- .preview-score {{
- font-size: 15px;
- font-weight: 800;
- }}
- /* Tab样式 */
- .tabs-nav {{
- background: white;
- padding: 0 30px;
- border-radius: 16px 16px 0 0;
- margin-bottom: 0;
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
- display: flex;
- gap: 10px;
- }}
- .tab-button {{
- padding: 15px 30px;
- border: none;
- background: transparent;
- color: #6b7280;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- border-bottom: 3px solid transparent;
- transition: all 0.3s;
- }}
- .tab-button:hover {{
- color: #667eea;
- background: rgba(102, 126, 234, 0.05);
- }}
- .tab-button.active {{
- color: #667eea;
- border-bottom-color: #667eea;
- background: rgba(102, 126, 234, 0.05);
- }}
- .tab-content {{
- display: none;
- background: white;
- padding: 30px;
- border-radius: 0 0 16px 16px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.15);
- }}
- .tab-content.active {{
- display: block;
- }}
- /* 人设结构样式 */
- .persona-structure-section h2 {{
- font-size: 28px;
- font-weight: 700;
- margin-bottom: 25px;
- color: #1a1a1a;
- }}
- /* 树状图样式 */
- .tree {{
- font-size: 14px;
- }}
- .tree ul {{
- padding-left: 30px;
- list-style: none;
- position: relative;
- }}
- .tree ul ul {{
- padding-left: 40px;
- }}
- .tree li {{
- position: relative;
- padding: 8px 0;
- }}
- .tree li::before {{
- content: "";
- position: absolute;
- top: 0;
- left: -20px;
- border-left: 2px solid #d1d5db;
- border-bottom: 2px solid #d1d5db;
- width: 20px;
- height: 20px;
- }}
- .tree li::after {{
- content: "";
- position: absolute;
- top: 20px;
- left: -20px;
- border-left: 2px solid #d1d5db;
- height: 100%;
- }}
- .tree li:last-child::after {{
- display: none;
- }}
- .tree > ul > li::before,
- .tree > ul > li::after {{
- display: none;
- }}
- .tree-node {{
- display: inline-flex;
- align-items: center;
- gap: 10px;
- padding: 12px 16px;
- border-radius: 8px;
- transition: all 0.3s;
- margin-bottom: 8px;
- }}
- .tree-node:hover {{
- transform: translateX(4px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
- }}
- .tree-node.level-1 {{
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-size: 18px;
- font-weight: 700;
- padding: 16px 20px;
- }}
- .tree-node.level-2 {{
- background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
- color: #1e40af;
- font-size: 16px;
- font-weight: 600;
- border: 2px solid #3b82f6;
- }}
- .tree-node.level-3 {{
- background: white;
- color: #374151;
- font-size: 14px;
- font-weight: 500;
- border: 1px solid #e5e7eb;
- }}
- .node-icon {{
- font-size: 20px;
- }}
- .tree-node.level-1 .node-icon {{
- font-size: 24px;
- }}
- .node-name {{
- flex: 1;
- }}
- .node-count {{
- background: rgba(255, 255, 255, 0.3);
- padding: 4px 12px;
- border-radius: 12px;
- font-size: 12px;
- font-weight: 600;
- }}
- .tree-node.level-1 .node-count {{
- background: rgba(255, 255, 255, 0.4);
- }}
- .tree-node.level-2 .node-count {{
- background: #bfdbfe;
- color: #1e3a8a;
- }}
- .tree-node.level-3 .node-count {{
- background: #dcfce7;
- color: #166534;
- }}
- .node-desc {{
- margin: 8px 0 8px 50px;
- padding: 12px 16px;
- background: #fffbeb;
- border-left: 3px solid #f59e0b;
- border-radius: 6px;
- color: #92400e;
- font-size: 13px;
- line-height: 1.7;
- }}
- .node-posts {{
- margin: 8px 0 8px 50px;
- padding: 12px 16px;
- background: #f0fdf4;
- border-left: 3px solid #10b981;
- border-radius: 6px;
- font-size: 12px;
- line-height: 1.8;
- }}
- .posts-label {{
- font-weight: 600;
- color: #15803d;
- margin-right: 8px;
- }}
- .posts-ids {{
- color: #166534;
- word-break: break-all;
- }}
- .posts-more {{
- color: #059669;
- font-weight: 600;
- margin-left: 8px;
- }}
- @media (max-width: 768px) {{
- .inspirations-grid {{
- grid-template-columns: 1fr;
- }}
- .header h1 {{
- font-size: 32px;
- }}
- .stats-overview {{
- grid-template-columns: repeat(2, 1fr);
- }}
- }}
- /* Step3 预览样式 */
- .step3-preview {{
- background: #fef3c7;
- padding: 12px;
- border-radius: 8px;
- margin-bottom: 10px;
- border-left: 3px solid #f59e0b;
- }}
- .step3-preview-header {{
- font-size: 12px;
- font-weight: 600;
- color: #92400e;
- margin-bottom: 8px;
- }}
- .step3-preview-list {{
- display: flex;
- flex-direction: column;
- gap: 8px;
- }}
- .step3-preview-item {{
- background: white;
- padding: 10px;
- border-radius: 6px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- }}
- .step3-point {{
- font-size: 13px;
- font-weight: 700;
- color: #1f2937;
- }}
- .step3-path {{
- font-size: 11px;
- color: #6b7280;
- line-height: 1.5;
- }}
- /* Step3 详情样式 */
- .step3-inspirations-list {{
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 10px;
- }}
- .step3-inspiration-item {{
- background: white;
- padding: 15px;
- border-radius: 8px;
- border-left: 4px solid #f59e0b;
- }}
- .step3-header {{
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
- }}
- .step3-rank {{
- font-size: 16px;
- font-weight: 800;
- color: #92400e;
- background: #fef3c7;
- padding: 4px 10px;
- border-radius: 6px;
- }}
- .step3-inspiration-item .step3-point {{
- font-size: 16px;
- font-weight: 700;
- color: #1f2937;
- flex: 1;
- }}
- .step3-inspiration-item .step3-path {{
- background: #f9fafb;
- padding: 8px 10px;
- border-radius: 6px;
- font-size: 12px;
- color: #4b5563;
- margin: 8px 0;
- }}
- .step3-desc {{
- font-size: 13px;
- color: #374151;
- line-height: 1.7;
- }}
- /* 卡片操作按钮样式 */
- .card-actions {{
- display: flex;
- gap: 10px;
- margin-top: 15px;
- }}
- .action-btn {{
- flex: 1;
- padding: 10px 16px;
- border: none;
- border-radius: 8px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- }}
- .action-btn:hover {{
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- }}
- .action-btn:active {{
- transform: translateY(0);
- }}
- .btn-step1 {{
- background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
- color: white;
- }}
- .btn-step1:hover {{
- background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
- }}
- .btn-step3 {{
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
- color: white;
- }}
- .btn-step3:hover {{
- background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
- }}
- </style>
- </head>
- <body>
- <div class="container">
- <div class="header">
- <h1>💡 灵感点分析可视化</h1>
- <div class="header-subtitle">基于HOW人设的灵感点匹配分析结果</div>
- <div class="stats-overview">
- <div class="stat-box">
- <div class="stat-label">灵感点总数</div>
- <div class="stat-value" id="totalCount">{total_count}</div>
- </div>
- <div class="stat-box excellent">
- <div class="stat-label">Top1优秀 (≥0.7)</div>
- <div class="stat-value" id="top1ExcellentCount">{top1_excellent_count}</div>
- </div>
- <div class="stat-box good">
- <div class="stat-label">Top1良好 (0.5-0.7)</div>
- <div class="stat-value" id="top1GoodCount">{top1_good_count}</div>
- </div>
- <div class="stat-box normal">
- <div class="stat-label">Top1一般 (0.3-0.5)</div>
- <div class="stat-value" id="top1NormalCount">{top1_normal_count}</div>
- </div>
- <div class="stat-box need-opt">
- <div class="stat-label">Top1待优化 (<0.3)</div>
- <div class="stat-value" id="top1NeedOptCount">{top1_need_opt_count}</div>
- </div>
- <div class="stat-box">
- <div class="stat-label">Top1平均分</div>
- <div class="stat-value" id="avgTop1Score">{avg_top1_score:.3f}</div>
- </div>
- </div>
- </div>
- <div class="tabs-nav">
- <button class="tab-button active" onclick="switchTab(event, 'tab-inspirations')">
- 灵感点分析
- </button>
- <button class="tab-button" onclick="switchTab(event, 'tab-persona')">
- 人设结构
- </button>
- </div>
- <div id="tab-inspirations" class="tab-content active">
- <div class="controls-section">
- <div class="search-box">
- <input type="text"
- id="searchInput"
- class="search-input"
- placeholder="🔍 搜索灵感点名称..."
- oninput="filterInspirations()">
- </div>
- <div class="sort-box">
- <span class="sort-label">排序方式:</span>
- <select id="sortSelect" class="sort-select" onchange="filterInspirations()">
- <option value="score-desc">Top1分数从高到低</option>
- <option value="score-asc">Top1分数从低到高</option>
- <option value="name-asc">名称A-Z</option>
- <option value="name-desc">名称Z-A</option>
- </select>
- </div>
- </div>
- <div class="inspirations-section">
- <div class="inspirations-grid">
- {cards_html_str}
- </div>
- </div>
- </div>
- <div id="tab-persona" class="tab-content">
- <div class="persona-structure-section">
- <h2>📚 人设结构</h2>
- {persona_structure_html}
- </div>
- </div>
- <div class="timestamp">生成时间: {timestamp}</div>
- <!-- Modal -->
- <div id="detailModal" class="modal-overlay" onclick="closeModalOnOverlay(event)">
- <div class="modal-content">
- <button class="modal-close" onclick="closeModal()">×</button>
- <div class="modal-body" id="modalBody">
- <!-- Content will be inserted here -->
- </div>
- </div>
- </div>
- </div>
- <script>
- {detail_modal_js}
- </script>
- </body>
- </html>'''
- # 写入文件
- output_file = Path(output_path)
- output_file.parent.mkdir(parents=True, exist_ok=True)
- with open(output_file, 'w', encoding='utf-8') as f:
- f.write(html_content)
- return str(output_file.absolute())
- def load_persona_data(persona_path: str) -> Dict[str, Any]:
- """
- 加载人设数据
- Args:
- persona_path: 人设JSON文件路径
- Returns:
- 人设数据字典
- """
- try:
- with open(persona_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- except Exception as e:
- print(f"警告: 读取人设文件失败: {e}")
- return {}
- def main():
- """主函数"""
- import sys
- # 配置路径
- inspiration_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点"
- posts_dir = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/作者历史帖子"
- persona_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/人设.json"
- output_path = "/Users/semsevens/Desktop/workspace/aaa/dev_3/data/阿里多多酱/out/人设_1110/how/灵感点可视化.html"
- print("=" * 60)
- print("灵感点分析可视化脚本")
- print("=" * 60)
- # 加载数据
- print("\n📂 正在加载灵感点数据...")
- inspirations_data = load_inspiration_points_data(inspiration_dir)
- print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点")
- print("\n📂 正在加载帖子数据...")
- posts_map = load_posts_data(posts_dir)
- print(f"✅ 成功加载 {len(posts_map)} 个帖子")
- print("\n📂 正在加载人设数据...")
- persona_data = load_persona_data(persona_path)
- print(f"✅ 成功加载人设数据")
- # 生成HTML
- print("\n🎨 正在生成可视化HTML...")
- result_path = generate_html(inspirations_data, posts_map, persona_data, output_path)
- print(f"\n✅ 可视化文件已生成!")
- print(f"📄 文件路径: {result_path}")
- print(f"\n💡 在浏览器中打开该文件即可查看可视化结果")
- print("=" * 60)
- if __name__ == "__main__":
- main()
|